diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index 2304d27cda8..eb53a01566d 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -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. diff --git a/changelogs/unreleased/4796-gautierdelorme-minor.md b/changelogs/unreleased/4796-gautierdelorme-minor.md new file mode 100644 index 00000000000..0ea08b5a282 --- /dev/null +++ b/changelogs/unreleased/4796-gautierdelorme-minor.md @@ -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. diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index f23726a0930..138c81a936c 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -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 diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index b61b359c7e6..78be7e3221d 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -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 diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 13c563a9ab3..bd6b7f9b928 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -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 diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index 7851d6a1557..631c3c34be6 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -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 diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 634873c3c11..5658dcc515b 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -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 diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index b5b26f06214..b128e16c604 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -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{ @@ -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, diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 53e60635a49..32395c8d38d 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -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. diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index 92b1ca73bd3..c5ae593fa53 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -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)) diff --git a/internal/envoy/v3/auth.go b/internal/envoy/v3/auth.go index 5ba085320b1..eab2022a374 100644 --- a/internal/envoy/v3/auth.go +++ b/internal/envoy/v3/auth.go @@ -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) } } diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index d9aef3ce289..64ae3a883c8 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -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{ @@ -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{ diff --git a/internal/featuretests/v3/downstreamvalidation_test.go b/internal/featuretests/v3/downstreamvalidation_test.go index 73f8f2b4a7f..9b022a6cdba 100644 --- a/internal/featuretests/v3/downstreamvalidation_test.go +++ b/internal/featuretests/v3/downstreamvalidation_test.go @@ -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() } diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 9be7712ace6..71f05232a0b 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -977,6 +977,23 @@
optionalClientCertificate
+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.
+