Skip to content

Commit

Permalink
Implement upstream tls backend verification with ca cert and optional
Browse files Browse the repository at this point in the history
subject alt name

Signed-off-by: Steve Sloka <[email protected]>
  • Loading branch information
stevesloka committed May 8, 2019
1 parent f4d8ff8 commit 5895bc5
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 20 deletions.
10 changes: 10 additions & 0 deletions apis/contour/v1beta1/ingressroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type Service struct {
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
// LB Algorithm to apply (see https://github.com/heptio/contour/blob/master/design/ingressroute-design.md#load-balancing)
Strategy string `json:"strategy,omitempty"`
// UpstreamValidation defines how to verify the backend service's certificate
UpstreamValidation *UpstreamValidation `json:"validation,omitempty"`
}

// Delegate allows for delegating VHosts to other IngressRoutes
Expand Down Expand Up @@ -141,6 +143,14 @@ type RetryPolicy struct {
PerTryTimeout string `json:"perTryTimeout,omitempty"`
}

// UpstreamValidation defines how to verify the backend service's certificate
type UpstreamValidation struct {
// Name of the Kubernetes secret be used to validate the certificate presented by the backend
CACertificate string `json:"caSecret"`
// Key which is expected to be present in the 'subjectAltName' of the presented certificate
SubjectName string `json:"subjectName"`
}

// Status reports the current state of the IngressRoute
type Status struct {
CurrentStatus string `json:"currentStatus"`
Expand Down
2 changes: 1 addition & 1 deletion apis/contour/v1beta1/tlscertificatedelegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type CertificateDelegation struct {
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TLSCertificateDelefgation is an TLS Certificate Delegation CRD specificiation.
// TLSCertificateDelegation is an TLS Certificate Delegation CRD specificiation.
// See design/tls-certificate-delegation.md for details.
type TLSCertificateDelegation struct {
metav1.TypeMeta `json:",inline"`
Expand Down
21 changes: 21 additions & 0 deletions apis/contour/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions docs/ingressroute.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,47 @@ spec:
permitInsecure: true
```

#### Upstream TLS

An IngressRoute route can proxy to an upstream TLS connection by first annotating the upstream Kubernetes service with: `contour.heptio.com/upstream-protocol.tls: "443,https"`.
This annoation tells Contour which port should be used for the TLS connection.
In this example, the upstream service is named `https` and uses port `443`.
Additionally, it is possible for Envoy to verify the backend service's certificate.
The service of an `IngressRoute` can optionally specify a `validation` struct which has a manditory `caSecret` key as well as an manditory `subjectName`.

Note: If spec.routes.services[].validation is present, spec.routes.services[].{name,port} must point to a service with a matching contour.heptio.com/upstream-protocol.tls Service annotation.

##### Sample YAML

```yaml
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
name: secure-backend
spec:
virtualhost:
fqdn: www.example.com
routes:
- match: /
services:
- name: service
port: 8443
validation:
caSecret: my-certificate-authority
subjectName: backend.example.com
```

##### Error conditions

If the `validation` spec is defined on a service, but the secret which it references does not exist, Contour will rejct the update and set the status of the `IngressRoute` object accordingly.
This is to help prevent the case of proxying to an upstream where validation is requested, but not yet available.

```yaml
Status:
Current Status: invalid
Description: route "/": service "tls-nginx": upstreamValidation requested but secret not found or misconfigured
```

#### TLS Certificate Delegation

In order to support wildcard certificates, TLS certificates for a `*.somedomain.com`, which are stored in a namespace controlled by the cluster administrator, Contour supports a facility known as TLS Certificate Delegation.
Expand Down
9 changes: 8 additions & 1 deletion internal/dag/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ func TestParseUpstreamProtocols(t *testing.T) {
"80": "h2",
},
},
"tls": {
a: map[string]string{fmt.Sprintf("%s.%s", annotationUpstreamProtocol, "tls"): "https,80"},
want: map[string]string{
"80": "tls",
"https": "tls",
},
},
"multiple value": {
a: map[string]string{fmt.Sprintf("%s.%s", annotationUpstreamProtocol, "h2"): "80,http,443,https"},
want: map[string]string{
Expand All @@ -102,7 +109,7 @@ func TestParseUpstreamProtocols(t *testing.T) {

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := parseUpstreamProtocols(tc.a, annotationUpstreamProtocol, "h2")
got := parseUpstreamProtocols(tc.a, annotationUpstreamProtocol, "h2", "tls")
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("parseUpstreamProtocols(%q): want: %v, got: %v", tc.a, tc.want, got)
}
Expand Down
29 changes: 26 additions & 3 deletions internal/dag/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"strconv"
"strings"

v1 "k8s.io/api/core/v1"
"k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/util/intstr"

Expand Down Expand Up @@ -468,7 +468,7 @@ func (b *builder) computeIngresses() {
be := httppath.Backend
m := meta{name: be.ServiceName, namespace: ing.Namespace}
if s := b.lookupHTTPService(m, be.ServicePort, "", nil); s != nil {
r.addHTTPService(s, 0)
r.addHTTPService(s, 0, nil)
}

// should we create port 80 routes for this ingress
Expand Down Expand Up @@ -668,7 +668,20 @@ func (b *builder) processRoutes(ir *ingressroutev1.IngressRoute, prefixMatch str
}
m := meta{name: service.Name, namespace: ir.Namespace}
if s := b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck); s != nil {
r.addHTTPService(s, service.Weight)
uv := b.lookupUpstreamValidation(service.UpstreamValidation, ir.Namespace)
if service.UpstreamValidation != nil {
if uv.CACertificate == nil {
// UpstreamValidation is requested, but cert is missing or not configured
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("route %q: service %q: upstreamValidation requested but secret not found or misconfigured", route.Match, service.Name), Vhost: host})
return
}
if len(uv.SubjectName) == 0 {
// UpstreamValidation is requested, but SAN is not provided
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("route %q: service %q: upstreamValidation requested but subject alt name not found or misconfigured", route.Match, service.Name), Vhost: host})
return
}
}
r.addHTTPService(s, service.Weight, uv)
}
}

Expand Down Expand Up @@ -714,6 +727,16 @@ func (b *builder) processRoutes(ir *ingressroutev1.IngressRoute, prefixMatch str
b.setStatus(Status{Object: ir, Status: StatusValid, Description: "valid IngressRoute", Vhost: host})
}

func (b *builder) lookupUpstreamValidation(uv *ingressroutev1.UpstreamValidation, namespace string) *UpstreamValidation {
if uv == nil {
return nil
}
return &UpstreamValidation{
CACertificate: b.lookupSecret(meta{name: uv.CACertificate, namespace: namespace}),
SubjectName: uv.SubjectName,
}
}

func (b *builder) processTCPProxy(ir *ingressroutev1.IngressRoute, visited []*ingressroutev1.IngressRoute, host string) {
visited = append(visited, ir)

Expand Down
23 changes: 20 additions & 3 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ type Route struct {

// Indicates that during forwarding, the matched prefix (or path) should be swapped with this value
PrefixRewrite string

// UpstreamValidation defines how to verify the backend service's certificate
UpstreamValidation *UpstreamValidation
}

// TimeoutPolicy defines the timeout request/idle
Expand All @@ -93,10 +96,21 @@ type RetryPolicy struct {
PerTryTimeout time.Duration
}

func (r *Route) addHTTPService(s *HTTPService, weight int) {
// UpstreamValidation defines how to validate the certificate on the upstream service
type UpstreamValidation struct {
// CACertificate holds a reference to the Secret containing the CA to be used to
// verify the upstream connection.
CACertificate *Secret
// SubjectName holds an optional subject name which Envoy will check against the
// certificate presented by the upstream.
SubjectName string
}

func (r *Route) addHTTPService(s *HTTPService, weight int, uv *UpstreamValidation) {
r.Clusters = append(r.Clusters, &Cluster{
Upstream: s,
Weight: weight,
Upstream: s,
Weight: weight,
UpstreamValidation: uv,
})
}

Expand Down Expand Up @@ -262,6 +276,9 @@ type Cluster struct {

// The relative weight of this Cluster compared to its siblings.
Weight int

// UpstreamValidation defines how to verify the backend service's certificate
UpstreamValidation *UpstreamValidation
}

func (c Cluster) Visit(f func(Vertex)) {
Expand Down
117 changes: 114 additions & 3 deletions internal/e2e/cds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ func TestClusterServiceTLSBackend(t *testing.T) {
}
rh.OnAdd(s1)

want := tlscluster("default/kuard/443/da39a3ee5e", "default/kuard/securebackend", "default_kuard_443")
want := tlscluster("default/kuard/443/da39a3ee5e", "default/kuard/securebackend", "default_kuard_443", nil, nil)

assertEqual(t, &v2.DiscoveryResponse{
VersionInfo: "2",
Expand All @@ -813,6 +813,117 @@ func TestClusterServiceTLSBackend(t *testing.T) {
}, streamCDS(t, cc))
}

// Test that contour correctly recognizes the "contour.heptio.com/upstream-protocol.tls"
// service annotation.
func TestClusterServiceTLSBackendCAValidation(t *testing.T) {
rh, cc, done := setup(t)
defer done()

rh.OnAdd(&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "kuard",
Namespace: "default",
Annotations: map[string]string{
"contour.heptio.com/upstream-protocol.tls": "securebackend,443",
},
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{{
Name: "securebackend",
Protocol: "TCP",
Port: 443,
TargetPort: intstr.FromInt(8080),
}},
},
})

secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
Data: secretdata(envoy.CACertificateKey, "key"),
}

rh.OnAdd(secret)

ir1 := &ingressroutev1.IngressRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
Spec: ingressroutev1.IngressRouteSpec{
VirtualHost: &ingressroutev1.VirtualHost{Fqdn: "www.example.com"},
Routes: []ingressroutev1.Route{{
Match: "/a",
Services: []ingressroutev1.Service{{
Name: "kuard",
Port: 443,
}},
}},
},
}

rh.OnAdd(ir1)

assertEqual(t, &v2.DiscoveryResponse{
VersionInfo: "3",
Resources: []types.Any{
any(t, tlscluster(
"default/kuard/443/da39a3ee5e",
"default/kuard/securebackend",
"default_kuard_443",
nil,
nil)),
},
TypeUrl: clusterType,
Nonce: "3",
}, streamCDS(t, cc))

ir2 := &ingressroutev1.IngressRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
Namespace: "default",
},
Spec: ingressroutev1.IngressRouteSpec{
VirtualHost: &ingressroutev1.VirtualHost{Fqdn: "www.example.com"},
Routes: []ingressroutev1.Route{{
Match: "/a",
Services: []ingressroutev1.Service{{
Name: "kuard",
Port: 443,
UpstreamValidation: &ingressroutev1.UpstreamValidation{
CACertificate: "foo",
SubjectName: "subjname",
},
}},
}},
},
}

rh.OnUpdate(ir1, ir2)

assertEqual(t, &v2.DiscoveryResponse{
VersionInfo: "4",
Resources: []types.Any{
any(t, tlscluster(
"default/kuard/443/98c0f31c72",
"default/kuard/securebackend",
"default_kuard_443",
[]byte("key"),
[]string{"subjname"})),
},
TypeUrl: clusterType,
Nonce: "4",
}, streamCDS(t, cc))
}

func secretdata(key, cert string) map[string][]byte {
return map[string][]byte{
key: []byte(cert),
}
}

func serviceWithAnnotations(ns, name string, annotations map[string]string, ports ...v1.ServicePort) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -852,9 +963,9 @@ func cluster(name, servicename, statName string) *v2.Cluster {
}
}

func tlscluster(name, servicename, statsName string) *v2.Cluster {
func tlscluster(name, servicename, statsName string, cert []byte, subjalt []string) *v2.Cluster {
c := cluster(name, servicename, statsName)
c.TlsContext = envoy.UpstreamTLSContext()
c.TlsContext = envoy.UpstreamTLSContext(cert, subjalt)
return c
}

Expand Down
Loading

0 comments on commit 5895bc5

Please sign in to comment.