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 <slokas@vmware.com>
  • Loading branch information
stevesloka committed Apr 25, 2019
1 parent bb84a11 commit 3ffa92b
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 37 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
CASecret string `json:"caSecret"`
// Ket which is expected to be present in the 'subjectAltName' of the presented certificate
SubjectName string `json:"subjectname,omitempty""`
}

// Status reports the current state of the IngressRoute
type Status struct {
CurrentStatus string `json:"currentStatus"`
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.

31 changes: 31 additions & 0 deletions docs/ingressroute.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,37 @@ 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 optional `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.
If the Service annotation is not present or incorrect, the route is rejected with an appropriate status message.

##### 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
```

#### 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
49 changes: 33 additions & 16 deletions internal/dag/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ type builder struct {
}

// lookupHTTPService returns a HTTPService that matches the meta and port supplied.
func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck) *HTTPService {
s := b.lookupService(m, port, strategy, hc)
func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck, uv *ingressroutev1.UpstreamValidation) *HTTPService {
s := b.lookupService(m, port, strategy, hc, uv)
switch s := s.(type) {
case *HTTPService:
return s
Expand All @@ -87,10 +87,10 @@ func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, strategy st
for i := range svc.Spec.Ports {
p := &svc.Spec.Ports[i]
if int(p.Port) == port.IntValue() {
return b.addHTTPService(svc, p, strategy, hc)
return b.addHTTPService(svc, p, strategy, hc, uv)
}
if port.String() == p.Name {
return b.addHTTPService(svc, p, strategy, hc)
return b.addHTTPService(svc, p, strategy, hc, uv)
}
}
return nil
Expand All @@ -101,8 +101,8 @@ func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, strategy st
}

// lookupTCPService returns a TCPService that matches the meta and port supplied.
func (b *builder) lookupTCPService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck) *TCPService {
s := b.lookupService(m, port, strategy, hc)
func (b *builder) lookupTCPService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck, uv *ingressroutev1.UpstreamValidation) *TCPService {
s := b.lookupService(m, port, strategy, hc, uv)
switch s := s.(type) {
case *TCPService:
return s
Expand All @@ -126,17 +126,18 @@ func (b *builder) lookupTCPService(m meta, port intstr.IntOrString, strategy str
return nil
}
}
func (b *builder) lookupService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck) Service {
func (b *builder) lookupService(m meta, port intstr.IntOrString, strategy string, hc *ingressroutev1.HealthCheck, uv *ingressroutev1.UpstreamValidation) Service {
if port.Type != intstr.Int {
// can't handle, give up
return nil
}
sm := servicemeta{
name: m.name,
namespace: m.namespace,
port: int32(port.IntValue()),
strategy: strategy,
healthcheck: healthcheckToString(hc),
name: m.name,
namespace: m.namespace,
port: int32(port.IntValue()),
strategy: strategy,
healthcheck: healthcheckToString(hc),
upstreamvalidation: upstreamValidationToString(uv),
}
s, ok := b.services[sm]
if !ok {
Expand All @@ -149,7 +150,11 @@ func healthcheckToString(hc *ingressroutev1.HealthCheck) string {
return fmt.Sprintf("%#v", hc)
}

func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, strategy string, hc *ingressroutev1.HealthCheck) *HTTPService {
func upstreamValidationToString(uv *ingressroutev1.UpstreamValidation) string {
return fmt.Sprintf("%#v", uv)
}

func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, strategy string, hc *ingressroutev1.HealthCheck, uv *ingressroutev1.UpstreamValidation) *HTTPService {
if b.services == nil {
b.services = make(map[servicemeta]Service)
}
Expand All @@ -171,13 +176,25 @@ func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, strategy
MaxRequests: parseAnnotation(svc.Annotations, annotationMaxRequests),
MaxRetries: parseAnnotation(svc.Annotations, annotationMaxRetries),
HealthCheck: hc,
UpstreamValidation: b.addUpstreamValidation(uv, svc),
},
Protocol: protocol,
}
b.services[s.toMeta()] = s
return s
}

func (b *builder) addUpstreamValidation(uv *ingressroutev1.UpstreamValidation, svc *v1.Service) *UpstreamValidation {
if uv != nil {
val := &UpstreamValidation{
Certificate: b.lookupSecret(meta{name: uv.CASecret, namespace: svc.Namespace}),
SubjectName: uv.SubjectName,
}
return val
}
return nil
}

func (b *builder) addTCPService(svc *v1.Service, port *v1.ServicePort, strategy string, hc *ingressroutev1.HealthCheck) *TCPService {
if b.services == nil {
b.services = make(map[servicemeta]Service)
Expand Down Expand Up @@ -466,7 +483,7 @@ func (b *builder) computeIngresses() {
r := prefixRoute(ing, prefix)
be := httppath.Backend
m := meta{name: be.ServiceName, namespace: ing.Namespace}
if s := b.lookupHTTPService(m, be.ServicePort, "", nil); s != nil {
if s := b.lookupHTTPService(m, be.ServicePort, "", nil, nil); s != nil {
r.addHTTPService(s, 0)
}

Expand Down Expand Up @@ -661,7 +678,7 @@ func (b *builder) processRoutes(ir *ingressroutev1.IngressRoute, prefixMatch str
return
}
m := meta{name: service.Name, namespace: ir.Namespace}
if s := b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck); s != nil {
if s := b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck, service.UpstreamValidation); s != nil {
r.addHTTPService(s, service.Weight)
}
}
Expand Down Expand Up @@ -722,7 +739,7 @@ func (b *builder) processTCPProxy(ir *ingressroutev1.IngressRoute, visited []*in
var proxy TCPProxy
for _, service := range tcpproxy.Services {
m := meta{name: service.Name, namespace: ir.Namespace}
s := b.lookupTCPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck)
s := b.lookupTCPService(m, intstr.FromInt(service.Port), service.Strategy, service.HealthCheck, service.UpstreamValidation)
if s == nil {
b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("tcpproxy: service %s/%s/%d: not found", ir.Namespace, service.Name, service.Port), Vhost: host})
return
Expand Down
3 changes: 2 additions & 1 deletion internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2724,6 +2724,7 @@ func TestBuilderLookupHTTPService(t *testing.T) {
port intstr.IntOrString
strategy string
healthcheck *ingressroutev1.HealthCheck
upstreamval *ingressroutev1.UpstreamValidation
want *HTTPService
}{
"lookup service by port number": {
Expand Down Expand Up @@ -2757,7 +2758,7 @@ func TestBuilderLookupHTTPService(t *testing.T) {
},
},
}
got := b.lookupHTTPService(tc.meta, tc.port, tc.strategy, tc.healthcheck)
got := b.lookupHTTPService(tc.meta, tc.port, tc.strategy, tc.healthcheck, tc.upstreamval)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatal(diff)
}
Expand Down
27 changes: 21 additions & 6 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

"github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
ingressroutev1 "github.com/heptio/contour/apis/contour/v1beta1"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
)

// A DAG represents a directed acylic graph of objects representing the relationship
Expand Down Expand Up @@ -93,6 +93,17 @@ type RetryPolicy struct {
PerTryTimeout time.Duration
}

// UpstreamValidation defines how to validate the certificate on the upstream service
type UpstreamValidation struct {
// Certificate holds a reference to the Secret containing the CA to be used to
// verify the upstream connection.
Certificate *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) {
r.Clusters = append(r.Clusters, &Cluster{
Upstream: s,
Expand Down Expand Up @@ -228,14 +239,18 @@ type TCPService struct {
MaxRetries int

HealthCheck *ingressroutev1.HealthCheck

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

type servicemeta struct {
name string
namespace string
port int32
strategy string
healthcheck string // %#v of *ingressroutev1.HealthCheck
name string
namespace string
port int32
strategy string
healthcheck string // %#v of *ingressroutev1.HealthCheck
upstreamvalidation string // %#v of *ingressroutev1.UpstreamValidation
}

func (s *TCPService) toMeta() servicemeta {
Expand Down
Loading

0 comments on commit 3ffa92b

Please sign in to comment.