Skip to content

Commit

Permalink
Add support for optional certificate validation (#4796)
Browse files Browse the repository at this point in the history
* Add support for optional certificate validation

Signed-off-by: Gautier Delorme <[email protected]>
  • Loading branch information
gautierdelorme authored Oct 21, 2022
1 parent a256481 commit ece8a24
Show file tree
Hide file tree
Showing 16 changed files with 371 additions and 2 deletions.
8 changes: 8 additions & 0 deletions apis/projectcontour/v1/httpproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,14 @@ type DownstreamValidation struct {
// certificate chain will be subject to validation by CRL.
// +optional
OnlyVerifyLeafCertCrl bool `json:"crlOnlyVerifyLeafCert"`

// OptionalClientCertificate when set to true will request a client certificate
// but allow the connection to continue if the client does not provide one.
// If a client certificate is sent, it will be verified according to the
// other properties, which includes disabling validation if
// SkipClientCertValidation is set. Defaults to false.
// +optional
OptionalClientCertificate bool `json:"optionalClientCertificate"`
}

// HTTPProxyStatus reports the current state of the HTTPProxy.
Expand Down
6 changes: 6 additions & 0 deletions changelogs/unreleased/4796-gautierdelorme-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Optional Client Certificate Validation

By default, when client certificate validation is configured, client certificates are required.
However, some applications might support different authentication schemes.
You can now set the `httpproxy.spec.virtualhost.tls.clientValidation.optionalClientCertificate` field to `true`. A client certificate will be requested, but the connection is allowed to continue if the client does not provide one.
If a client certificate is sent, it will be verified according to the other properties, which includes disabling validations if `httpproxy.spec.virtualhost.tls.clientValidation.skipClientCertValidation` is set.
8 changes: 8 additions & 0 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5766,6 +5766,14 @@ spec:
secrets are limited to 1MiB in size.
minLength: 1
type: string
optionalClientCertificate:
description: OptionalClientCertificate when set to true
will request a client certificate but allow the connection
to continue if the client does not provide one. If a
client certificate is sent, it will be verified according
to the other properties, which includes disabling validation
if SkipClientCertValidation is set. Defaults to false.
type: boolean
skipClientCertValidation:
description: SkipClientCertValidation disables downstream
client certificate validation. Defaults to false. This
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5975,6 +5975,14 @@ spec:
secrets are limited to 1MiB in size.
minLength: 1
type: string
optionalClientCertificate:
description: OptionalClientCertificate when set to true
will request a client certificate but allow the connection
to continue if the client does not provide one. If a
client certificate is sent, it will be verified according
to the other properties, which includes disabling validation
if SkipClientCertValidation is set. Defaults to false.
type: boolean
skipClientCertValidation:
description: SkipClientCertValidation disables downstream
client certificate validation. Defaults to false. This
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour-gateway-provisioner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5780,6 +5780,14 @@ spec:
secrets are limited to 1MiB in size.
minLength: 1
type: string
optionalClientCertificate:
description: OptionalClientCertificate when set to true
will request a client certificate but allow the connection
to continue if the client does not provide one. If a
client certificate is sent, it will be verified according
to the other properties, which includes disabling validation
if SkipClientCertValidation is set. Defaults to false.
type: boolean
skipClientCertValidation:
description: SkipClientCertValidation disables downstream
client certificate validation. Defaults to false. This
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour-gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5981,6 +5981,14 @@ spec:
secrets are limited to 1MiB in size.
minLength: 1
type: string
optionalClientCertificate:
description: OptionalClientCertificate when set to true
will request a client certificate but allow the connection
to continue if the client does not provide one. If a
client certificate is sent, it will be verified according
to the other properties, which includes disabling validation
if SkipClientCertValidation is set. Defaults to false.
type: boolean
skipClientCertValidation:
description: SkipClientCertValidation disables downstream
client certificate validation. Defaults to false. This
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5975,6 +5975,14 @@ spec:
secrets are limited to 1MiB in size.
minLength: 1
type: string
optionalClientCertificate:
description: OptionalClientCertificate when set to true
will request a client certificate but allow the connection
to continue if the client does not provide one. If a
client certificate is sent, it will be verified according
to the other properties, which includes disabling validation
if SkipClientCertValidation is set. Defaults to false.
type: boolean
skipClientCertValidation:
description: SkipClientCertValidation disables downstream
client certificate validation. Defaults to false. This
Expand Down
61 changes: 61 additions & 0 deletions internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6521,6 +6521,35 @@ func TestDAGInsert(t *testing.T) {
},
}

// proxy24 is downstream validation, optional cert validation
proxy24 := &contour_api_v1.HTTPProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "example-com",
Namespace: "default",
},
Spec: contour_api_v1.HTTPProxySpec{
VirtualHost: &contour_api_v1.VirtualHost{
Fqdn: "example.com",
TLS: &contour_api_v1.TLS{
SecretName: sec1.Name,
ClientValidation: &contour_api_v1.DownstreamValidation{
CACertificate: cert1.Name,
OptionalClientCertificate: true,
},
},
},
Routes: []contour_api_v1.Route{{
Conditions: []contour_api_v1.MatchCondition{{
Prefix: "/",
}},
Services: []contour_api_v1.Service{{
Name: s1.Name,
Port: 8080,
}},
}},
},
}

// invalid because tcpproxy both includes another and
// has a list of services.
proxy37 := &contour_api_v1.HTTPProxy{
Expand Down Expand Up @@ -9774,6 +9803,38 @@ func TestDAGInsert(t *testing.T) {
},
),
},
"insert httpproxy w/ tls termination with optional client validation": {
objs: []interface{}{
proxy24, s1, sec1, cert1, crl,
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("example.com", routeUpgrade("/", service(s1))),
),
}, &Listener{
Name: HTTPS_LISTENER_NAME,
Port: 443,
SecureVirtualHosts: securevirtualhosts(
&SecureVirtualHost{
VirtualHost: VirtualHost{
Name: "example.com",
Routes: routes(
routeUpgrade("/", service(s1))),
},
MinTLSVersion: "1.2",
Secret: secret(sec1),
DownstreamValidation: &PeerValidationContext{
CACertificate: &Secret{Object: cert1},
OptionalClientCertificate: true,
},
},
),
},
),
},
"insert httpproxy with downstream verification, missing ca certificate": {
objs: []interface{}{
proxy18, s1, sec1,
Expand Down
3 changes: 3 additions & 0 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ type PeerValidationContext struct {
// OnlyVerifyLeafCertCrl when set to true, only the certificate at the end of the
// certificate chain will be subject to validation by CRL.
OnlyVerifyLeafCertCrl bool
// OptionalClientCertificate when set to true will ensure Envoy does not require
// that the client sends a certificate but if one is sent it will process it.
OptionalClientCertificate bool
}

// GetCACertificate returns the CA certificate from PeerValidationContext.
Expand Down
3 changes: 2 additions & 1 deletion internal/dag/httpproxy_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ func (p *HTTPProxyProcessor) computeHTTPProxy(proxy *contour_api_v1.HTTPProxy) {
// Fill in DownstreamValidation when external client validation is enabled.
if tls.ClientValidation != nil {
dv := &PeerValidationContext{
SkipClientCertValidation: tls.ClientValidation.SkipClientCertValidation,
SkipClientCertValidation: tls.ClientValidation.SkipClientCertValidation,
OptionalClientCertificate: tls.ClientValidation.OptionalClientCertificate,
}
if tls.ClientValidation.CACertificate != "" {
secretName := k8s.NamespacedNameFrom(tls.ClientValidation.CACertificate, k8s.DefaultNamespace(proxy.Namespace))
Expand Down
2 changes: 1 addition & 1 deletion internal/envoy/v3/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func DownstreamTLSContext(serverSecret *dag.Secret, tlsMinProtoVersion envoy_v3_
peerValidationContext.GetCRL(), peerValidationContext.OnlyVerifyLeafCertCrl)
if vc != nil {
context.CommonTlsContext.ValidationContextType = vc
context.RequireClientCertificate = protobuf.Bool(true)
context.RequireClientCertificate = protobuf.Bool(!peerValidationContext.OptionalClientCertificate)
}
}

Expand Down
26 changes: 26 additions & 0 deletions internal/envoy/v3/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,20 @@ func TestDownstreamTLSContext(t *testing.T) {
},
},
}
peerValidationContextOptionalClientCertValidationWithCA := &dag.PeerValidationContext{
CACertificate: &dag.Secret{
Object: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Data: map[string][]byte{
dag.CACertificateKey: ca,
},
},
},
OptionalClientCertificate: true,
}
peerValidationContextWithCRLCheck := &dag.PeerValidationContext{
CACertificate: &dag.Secret{
Object: &v1.Secret{
Expand Down Expand Up @@ -465,6 +479,18 @@ func TestDownstreamTLSContext(t *testing.T) {
RequireClientCertificate: protobuf.Bool(true),
},
},
"optional client cert validation with ca": {
DownstreamTLSContext(serverSecret, envoy_tls_v3.TlsParameters_TLSv1_2, cipherSuites, peerValidationContextOptionalClientCertValidationWithCA, "h2", "http/1.1"),
&envoy_tls_v3.DownstreamTlsContext{
CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
TlsParams: tlsParams,
TlsCertificateSdsSecretConfigs: tlsCertificateSdsSecretConfigs,
AlpnProtocols: alpnProtocols,
ValidationContextType: validationContext,
},
RequireClientCertificate: protobuf.Bool(false),
},
},
"Downstream validation with CRL check": {
DownstreamTLSContext(serverSecret, envoy_tls_v3.TlsParameters_TLSv1_2, cipherSuites, peerValidationContextWithCRLCheck, "h2", "http/1.1"),
&envoy_tls_v3.DownstreamTlsContext{
Expand Down
49 changes: 49 additions & 0 deletions internal/featuretests/v3/downstreamvalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,53 @@ func TestDownstreamTLSCertificateValidation(t *testing.T) {
statsListener(),
),
}).Status(proxy5).IsValid()

proxy6 := fixture.NewProxy("example.com").
WithSpec(contour_api_v1.HTTPProxySpec{
VirtualHost: &contour_api_v1.VirtualHost{
Fqdn: "example.com",
TLS: &contour_api_v1.TLS{
SecretName: serverTLSSecret.Name,
ClientValidation: &contour_api_v1.DownstreamValidation{
CACertificate: clientCASecret.Name,
OptionalClientCertificate: true,
},
},
},
Routes: []contour_api_v1.Route{{
Services: []contour_api_v1.Service{{
Name: "kuard",
Port: 8080,
}},
}},
})
rh.OnUpdate(proxy5, proxy6)

ingressHTTPSOptionalVerify := &envoy_listener_v3.Listener{
Name: "ingress_https",
Address: envoy_v3.SocketAddress("0.0.0.0", 8443),
ListenerFilters: envoy_v3.ListenerFilters(
envoy_v3.TLSInspector(),
),
FilterChains: appendFilterChains(
filterchaintls("example.com", serverTLSSecret,
httpsFilterFor("example.com"),
&dag.PeerValidationContext{
CACertificate: &dag.Secret{
Object: clientCASecret,
},
OptionalClientCertificate: true,
},
"h2", "http/1.1",
),
),
SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(),
}
c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{
Resources: resources(t,
defaultHTTPListener(),
ingressHTTPSOptionalVerify,
statsListener(),
),
}).Status(proxy6).IsValid()
}
17 changes: 17 additions & 0 deletions site/content/docs/main/config/api-reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,23 @@ <h3 id="projectcontour.io/v1.DownstreamValidation">DownstreamValidation
certificate chain will be subject to validation by CRL.</p>
</td>
</tr>
<tr>
<td style="white-space:nowrap">
<code>optionalClientCertificate</code>
<br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>OptionalClientCertificate when set to true will request a client certificate
but allow the connection to continue if the client does not provide one.
If a client certificate is sent, it will be verified according to the
other properties, which includes disabling validation if
SkipClientCertValidation is set. Defaults to false.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="projectcontour.io/v1.ExtensionServiceReference">ExtensionServiceReference
Expand Down
21 changes: 21 additions & 0 deletions site/content/docs/main/config/tls-termination.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ Its mandatory attribute `caSecret` contains a name of an existing Kubernetes Sec
The data value of the key `ca.crt` must be a PEM-encoded certificate bundle and it must contain all the trusted CA certificates that are to be used for validating the client certificate.
If the Opaque Secret also contains one of either `tls.crt` or `tls.key` keys, it will be ignored.

By default, client certificates are required but some applications might support different authentication schemes. In that case you can set the `optionalClientCertificate` field to `true`. A client certificate will be requested, but the connection is allowed to continue if the client does not provide one. If a client certificate is sent, it will be verified according to the other properties, which includes disabling validations if `skipClientCertValidation` is set.

```yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: with-optional-client-auth
spec:
virtualhost:
fqdn: www.example.com
tls:
secretName: secret
clientValidation:
caSecret: client-root-ca
optionalClientCertificate: true
routes:
- services:
- name: s1
port: 80
```

When using external authorization, it may be desirable to use an external authorization server to validate client certificates on requests, rather than the Envoy proxy.

```yaml
Expand Down
Loading

0 comments on commit ece8a24

Please sign in to comment.