Skip to content

Commit

Permalink
WIP external client authentication
Browse files Browse the repository at this point in the history
Updates after 1st review.

Adds support for authentication of external clients (downstream) by validating
their client certificates against trusted CA certificate.

Signed-off-by: Tero Saarni <tero.saarni@est.tech>
  • Loading branch information
tsaarni committed Mar 6, 2020
1 parent 7337744 commit 22f8d28
Show file tree
Hide file tree
Showing 21 changed files with 422 additions and 234 deletions.
7 changes: 5 additions & 2 deletions apis/projectcontour/v1/httpproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ type TLS struct {
// backing cluster.
// +optional
Passthrough bool `json:"passthrough,omitempty"`
// ClientValidation defines how to verify the client certificate
// ClientValidation defines how to verify the client certificate. This setting:
// 1. Enables TLS client certificate validation.
// 2. Requires clients to present a TLS certificate (i.e. not optional validation).
// 3. Specifies how the client certificate will be validated.
// +optional
ClientValidation *DownstreamValidation `json:"clientValidation,omitempty"`
}
Expand Down Expand Up @@ -375,7 +378,7 @@ type UpstreamValidation struct {
SubjectName string `json:"subjectName"`
}

// DownstreamValidation defines how to verify the client certificate
// DownstreamValidation defines how to verify the client certificate.
type DownstreamValidation struct {
// Name of the Kubernetes secret be used to validate the certificate presented by the client
CACertificate string `json:"caSecret"`
Expand Down
14 changes: 10 additions & 4 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,11 @@ spec:
secret must contain a matching certificate
properties:
clientValidation:
description: ClientValidation defines how to verify the client
certificate
description: 'ClientValidation defines how to verify the client
certificate. This setting: 1. Enables TLS client certificate
validation. 2. Requires clients to present a TLS certificate
(i.e. not optional validation). 3. Specifies how the client
certificate will be validated.'
properties:
caSecret:
description: Name of the Kubernetes secret be used to validate
Expand Down Expand Up @@ -1100,8 +1103,11 @@ spec:
secret must contain a matching certificate
properties:
clientValidation:
description: ClientValidation defines how to verify the client
certificate
description: 'ClientValidation defines how to verify the client
certificate. This setting: 1. Enables TLS client certificate
validation. 2. Requires clients to present a TLS certificate
(i.e. not optional validation). 3. Specifies how the client
certificate will be validated.'
properties:
caSecret:
description: Name of the Kubernetes secret be used to validate
Expand Down
14 changes: 10 additions & 4 deletions examples/render/contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,11 @@ spec:
secret must contain a matching certificate
properties:
clientValidation:
description: ClientValidation defines how to verify the client
certificate
description: 'ClientValidation defines how to verify the client
certificate. This setting: 1. Enables TLS client certificate
validation. 2. Requires clients to present a TLS certificate
(i.e. not optional validation). 3. Specifies how the client
certificate will be validated.'
properties:
caSecret:
description: Name of the Kubernetes secret be used to validate
Expand Down Expand Up @@ -1174,8 +1177,11 @@ spec:
secret must contain a matching certificate
properties:
clientValidation:
description: ClientValidation defines how to verify the client
certificate
description: 'ClientValidation defines how to verify the client
certificate. This setting: 1. Enables TLS client certificate
validation. 2. Requires clients to present a TLS certificate
(i.e. not optional validation). 3. Specifies how the client
certificate will be validated.'
properties:
caSecret:
description: Name of the Kubernetes secret be used to validate
Expand Down
17 changes: 5 additions & 12 deletions internal/contour/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,21 +366,14 @@ func (v *listenerVisitor) visit(vertex dag.Vertex) {
alpnProtos = nil // do not offer ALPN
}

var ca []byte
var subjectName string
if vh.DownstreamValidation != nil {
ca = vh.DownstreamValidation.ValidationContextCACert()
subjectName = vh.DownstreamValidation.SubjectName
}

fc := envoy.FilterChainTLS(
vh.VirtualHost.Name,
vh.Secret,
ca,
subjectName,
envoy.DownstreamTLSContext(
vh.Secret,
max(v.ListenerVisitorConfig.minProtoVersion(), vh.MinProtoVersion), // Choose the higher of the configured or requested tls version.
vh.DownstreamValidation,
alpnProtos...),
filters,
max(v.ListenerVisitorConfig.minProtoVersion(), vh.MinProtoVersion), // choose the higher of the configured or requested tls version
alpnProtos...,
)

v.listeners[ENVOY_HTTPS_LISTENER].FilterChains = append(v.listeners[ENVOY_HTTPS_LISTENER].FilterChains, fc)
Expand Down
13 changes: 12 additions & 1 deletion internal/contour/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
ingressroutev1 "github.com/projectcontour/contour/apis/contour/v1beta1"
projcontour "github.com/projectcontour/contour/apis/projectcontour/v1"
"github.com/projectcontour/contour/internal/assert"
"github.com/projectcontour/contour/internal/dag"
"github.com/projectcontour/contour/internal/envoy"
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
Expand Down Expand Up @@ -1067,8 +1068,18 @@ func TestListenerVisit(t *testing.T) {
}

func transportSocket(tlsMinProtoVersion envoy_api_v2_auth.TlsParameters_TlsProtocol, alpnprotos ...string) *envoy_api_v2_core.TransportSocket {
secret := &dag.Secret{
Object: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Type: "kubernetes.io/tls",
Data: secretdata(CERTIFICATE, RSA_PRIVATE_KEY),
},
}
return envoy.DownstreamTLSTransportSocket(
envoy.DownstreamTLSContext("default/secret/68621186db", tlsMinProtoVersion, nil, "", alpnprotos...),
envoy.DownstreamTLSContext(secret, tlsMinProtoVersion, nil, alpnprotos...),
)
}

Expand Down
27 changes: 13 additions & 14 deletions internal/dag/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,13 +489,12 @@ func (b *Builder) computeHTTPProxy(proxy *projcontour.HTTPProxy) {
svhost.Secret = sec
svhost.MinProtoVersion = MinProtoVersion(proxy.Spec.VirtualHost.TLS.MinimumProtocolVersion)

// Fill in DownstreamValidation when external client validation is enabled
// Fill in DownstreamValidation when external client validation is enabled.
if tls.ClientValidation != nil {
dv, err := b.lookupDownstreamValidation(tls.ClientValidation,
proxy.Namespace,
fmt.Sprintf("HTTPProxy: %q", proxy.Name))
dv, err := b.lookupDownstreamValidation(tls.ClientValidation, proxy.Namespace)
if err != nil {
sw.SetInvalid("%s", err)
sw.SetInvalid("HTTPProxy [%s]: TLS downstream validation policy error: %s", proxy.Name, err)
return
}
svhost.DownstreamValidation = dv
}
Expand Down Expand Up @@ -795,7 +794,7 @@ func (b *Builder) computeRoutes(sw *ObjectStatusWriter, proxy *projcontour.HTTPP
return nil
}

var uv *ValidationContext
var uv *PeerValidationContext
if protocol == "tls" {
// we can only validate TLS connections to services that talk TLS
uv, err = b.lookupUpstreamValidation(service.UpstreamValidation, proxy.Namespace)
Expand Down Expand Up @@ -1005,7 +1004,7 @@ func (b *Builder) processIngressRoutes(sw *ObjectStatusWriter, ir *ingressroutev
return
}

var uv *ValidationContext
var uv *PeerValidationContext
var err error
if s.Protocol == "tls" {
// we can only validate TLS connections to services that talk TLS
Expand Down Expand Up @@ -1076,7 +1075,7 @@ func (b *Builder) processIngressRoutes(sw *ObjectStatusWriter, ir *ingressroutev
sw.SetValid()
}

func (b *Builder) lookupUpstreamValidation(uv *projcontour.UpstreamValidation, namespace string) (*UpstreamValidation, error) {
func (b *Builder) lookupUpstreamValidation(uv *projcontour.UpstreamValidation, namespace string) (*PeerValidationContext, error) {
if uv == nil {
// no upstream validation requested, nothing to do
return nil, nil
Expand All @@ -1093,25 +1092,25 @@ func (b *Builder) lookupUpstreamValidation(uv *projcontour.UpstreamValidation, n
return nil, errors.New("missing subject alternative name")
}

return &ValidationContext{
return &PeerValidationContext{
CACertificate: cacert,
SubjectName: uv.SubjectName,
}, nil
}

func (b *Builder) lookupDownstreamValidation(vc *projcontour.DownstreamValidation, namespace string, errorContext string) (*ValidationContext, error) {
func (b *Builder) lookupDownstreamValidation(vc *projcontour.DownstreamValidation, namespace string) (*PeerValidationContext, error) {
if vc == nil {
// no downstream validation requested, nothing to do
// No downstream validation requested, nothing to do.
return nil, nil
}

cacert := b.lookupSecret(Meta{name: vc.CACertificate, namespace: namespace}, validCA)
if cacert == nil {
// ValidationContext is requested, but cert is missing or not configured
return nil, fmt.Errorf("%s downstreamValidation requested but secret not found or misconfigured", errorContext)
// ValidationContext is requested, but cert is missing or not configured.
return nil, fmt.Errorf("secret not found or misconfigured")
}

return &ValidationContext{
return &PeerValidationContext{
CACertificate: cacert,
}, nil
}
Expand Down
68 changes: 65 additions & 3 deletions internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,33 @@ func TestDAGInsert(t *testing.T) {
},
}

proxy18 := &projcontour.HTTPProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "example-com",
Namespace: "default",
},
Spec: projcontour.HTTPProxySpec{
VirtualHost: &projcontour.VirtualHost{
Fqdn: "example.com",
TLS: &projcontour.TLS{
SecretName: sec1.Name,
ClientValidation: &projcontour.DownstreamValidation{
CACertificate: cert1.Name,
},
},
},
Routes: []projcontour.Route{{
Conditions: []projcontour.Condition{{
Prefix: "/",
}},
Services: []projcontour.Service{{
Name: "kuard",
Port: 8080,
}},
}},
},
}

// proxy10 has a websocket route
proxy10 := &projcontour.HTTPProxy{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -4290,7 +4317,7 @@ func TestDAGInsert(t *testing.T) {
Protocol: "tls",
},
Protocol: "tls",
UpstreamValidation: &ValidationContext{
UpstreamValidation: &PeerValidationContext{
CACertificate: secret(cert1),
SubjectName: "example.com",
},
Expand Down Expand Up @@ -5240,7 +5267,7 @@ func TestDAGInsert(t *testing.T) {
},
),
},
"insert httpproxy expecting verification": {
"insert httpproxy expecting upstream verification": {
objs: []interface{}{
cert1, proxy17, s1a,
},
Expand All @@ -5258,7 +5285,7 @@ func TestDAGInsert(t *testing.T) {
Protocol: "tls",
},
Protocol: "tls",
UpstreamValidation: &ValidationContext{
UpstreamValidation: &PeerValidationContext{
CACertificate: secret(cert1),
SubjectName: "example.com",
},
Expand All @@ -5269,6 +5296,41 @@ func TestDAGInsert(t *testing.T) {
},
),
},
"insert httpproxy with downstream verification": {
objs: []interface{}{
cert1, proxy18, s1, sec1,
},
want: listeners(
&Listener{
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("example.com", routeUpgrade("/", service(s1))),
),
}, &Listener{
Port: 443,
VirtualHosts: virtualhosts(
&SecureVirtualHost{
VirtualHost: VirtualHost{
Name: "example.com",
routes: routes(
routeUpgrade("/", service(s1))),
},
MinProtoVersion: envoy_api_v2_auth.TlsParameters_TLSv1_1,
Secret: secret(sec1),
DownstreamValidation: &PeerValidationContext{
CACertificate: &Secret{Object: cert1},
},
},
),
},
),
},
"insert httpproxy with downstream verification, missing ca certificate": {
objs: []interface{}{
proxy18, s1, sec1,
},
want: listeners(),
},
"insert httpproxy with invalid tcpproxy": {
objs: []interface{}{proxy37, s1},
want: listeners(),
Expand Down
31 changes: 22 additions & 9 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ type HeaderValue struct {
Value string
}

// ValidationContext defines how to validate the certificate on the upstream service
type ValidationContext struct {
// PeerValidationContext defines how to validate the certificate on the upstream service.
type PeerValidationContext struct {
// CACertificate holds a reference to the Secret containing the CA to be used to
// verify the upstream connection.
CACertificate *Secret
Expand All @@ -191,12 +191,25 @@ type ValidationContext struct {
SubjectName string
}

// CACertificateKey stores the key name for the TLS validation secret cert
// CACertificateKey stores the key name for the TLS validation secret cert.
const CACertificateKey = "ca.crt"

// ValidationContextCACert returns the CA certificate from ValidationContext
func (vc *ValidationContext) ValidationContextCACert() []byte {
return vc.CACertificate.Object.Data[CACertificateKey]
// GetCACertificate returns the CA certificate from PeerValidationContext.
func (pvc *PeerValidationContext) GetCACertificate() []byte {
if pvc == nil || pvc.CACertificate == nil {
// No validation required.
return nil
}
return pvc.CACertificate.Object.Data[CACertificateKey]
}

// GetSubjectName returns the SubjectName from PeerValidationContext.
func (pvc *PeerValidationContext) GetSubjectName() string {
if pvc == nil {
// No validation required.
return ""
}
return pvc.SubjectName
}

func (r *Route) Visit(f func(Vertex)) {
Expand Down Expand Up @@ -253,8 +266,8 @@ type SecureVirtualHost struct {
// Service to TCP proxy all incoming connections.
*TCPProxy

// DownstreamValidation defines how to verify the client's certificate
DownstreamValidation *ValidationContext
// DownstreamValidation defines how to verify the client's certificate.
DownstreamValidation *PeerValidationContext
}

func (s *SecureVirtualHost) Visit(f func(Vertex)) {
Expand Down Expand Up @@ -381,7 +394,7 @@ type Cluster struct {
Protocol string

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

// The load balancer type to use when picking a host in the cluster.
// See https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/cds.proto#envoy-api-enum-cluster-lbpolicy
Expand Down
Loading

0 comments on commit 22f8d28

Please sign in to comment.