Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for optional certificate validation #4796

Merged
merged 3 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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