diff --git a/docs/en/latest/concepts/apisix_tls.md b/docs/en/latest/concepts/apisix_tls.md index 5cd73fac4d..2dc7f12985 100644 --- a/docs/en/latest/concepts/apisix_tls.md +++ b/docs/en/latest/concepts/apisix_tls.md @@ -51,3 +51,33 @@ Make sure that the `hosts` field is accurate. APISIX uses the `host` field to ma ::: APISIX Ingress will watch the secret resources referred by `ApisixTls` objects and re-translates it to APISIX resources if they are changed. + +## Bypassing MTLS based on regular expression matching against URI + +::: note +This feature is only supported with APISIX version 3.4 or above. +::: + +APISIX allows configuring an URI whitelist to bypass MTLS. If the URI of a request is in the whitelist, then the client certificate will not be checked. Note that other URIs of the associated SNI will get HTTP 400 response instead of alert error in the SSL handshake phase, if the client certificate is missing or invalid. + +The below example creates an APISIX ssl resource where MTLS is bypassed for any route that starts with `/ip`. + +```yaml +apiVersion: %s +kind: ApisixTls +metadata: + name: my-tls +spec: + hosts: + - httpbin.org + secret: + name: my-secret + namespace: default + client: + caSecret: + name: ca-secret + namespace: default + depth: 10 + skip_mtls_uri_regex: + - /ip.* +``` diff --git a/docs/en/latest/references/apisix_tls_v2.md b/docs/en/latest/references/apisix_tls_v2.md index 8084bc22d7..22c335eaff 100644 --- a/docs/en/latest/references/apisix_tls_v2.md +++ b/docs/en/latest/references/apisix_tls_v2.md @@ -42,3 +42,4 @@ See the [definition](https://github.com/apache/apisix-ingress-controller/blob/ma | client.caSecret.name | string | Name of the Secret related to the certificate provided by the client. | | client.caSecret.namespace | string | Namespace of the Secret related to the certificate. | | client.depth | int | The maximum length of the certificate chain. | +| client.skip_mtls_uri_regex | array | List of uri regular expression to skip mtls. | diff --git a/docs/en/latest/tutorials/mtls-bypass.md b/docs/en/latest/tutorials/mtls-bypass.md new file mode 100644 index 0000000000..64a7873de8 --- /dev/null +++ b/docs/en/latest/tutorials/mtls-bypass.md @@ -0,0 +1,50 @@ +--- +title: MTLS bypass based on regular expression matching against URI +--- + + + +APISIX allows configuring an URI whitelist to bypass MTLS. If the URI of a request is in the whitelist, then the client certificate will not be checked. Note that other URIs of the associated SNI will get HTTP 400 response instead of alert error in the SSL handshake phase, if the client certificate is missing or invalid. + +::: note +This feature is only available in APISIX version 3.4 and above. +::: + +The below example creates an APISIX ssl resource where MTLS is bypassed for any route that starts with `/ip`. + +```yaml +apiVersion: %s +kind: ApisixTls +metadata: + name: my-tls +spec: + hosts: + - httpbin.org + secret: + name: my-secret + namespace: default + client: + caSecret: + name: ca-secret + namespace: default + depth: 10 + skip_mtls_uri_regex: + - /ip.* +``` diff --git a/pkg/kube/apisix/apis/config/v2/types.go b/pkg/kube/apisix/apis/config/v2/types.go index 19c9ff5130..1edb8e5521 100644 --- a/pkg/kube/apisix/apis/config/v2/types.go +++ b/pkg/kube/apisix/apis/config/v2/types.go @@ -784,8 +784,9 @@ type ApisixSecret struct { // ApisixMutualTlsClientConfig describes the mutual TLS CA and verify depth type ApisixMutualTlsClientConfig struct { - CASecret ApisixSecret `json:"caSecret,omitempty" yaml:"caSecret,omitempty"` - Depth int `json:"depth,omitempty" yaml:"depth,omitempty"` + CASecret ApisixSecret `json:"caSecret,omitempty" yaml:"caSecret,omitempty"` + Depth int `json:"depth,omitempty" yaml:"depth,omitempty"` + SkipMTLSUriRegex []string `json:"skip_mtls_uri_regex,omitempty" yaml:"skip_mtls_uri_regex, omitempty"` } // +genclient diff --git a/pkg/kube/apisix/apis/config/v2/zz_generated.deepcopy.go b/pkg/kube/apisix/apis/config/v2/zz_generated.deepcopy.go index d12816d3ef..3addec4540 100644 --- a/pkg/kube/apisix/apis/config/v2/zz_generated.deepcopy.go +++ b/pkg/kube/apisix/apis/config/v2/zz_generated.deepcopy.go @@ -721,6 +721,11 @@ func (in *ApisixGlobalRuleSpec) DeepCopy() *ApisixGlobalRuleSpec { func (in *ApisixMutualTlsClientConfig) DeepCopyInto(out *ApisixMutualTlsClientConfig) { *out = *in out.CASecret = in.CASecret + if in.SkipMTLSUriRegex != nil { + in, out := &in.SkipMTLSUriRegex, &out.SkipMTLSUriRegex + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -1340,7 +1345,7 @@ func (in *ApisixTlsSpec) DeepCopyInto(out *ApisixTlsSpec) { if in.Client != nil { in, out := &in.Client, &out.Client *out = new(ApisixMutualTlsClientConfig) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/pkg/providers/apisix/translation/apisix_ssl.go b/pkg/providers/apisix/translation/apisix_ssl.go index 9737603125..1a958a1583 100644 --- a/pkg/providers/apisix/translation/apisix_ssl.go +++ b/pkg/providers/apisix/translation/apisix_ssl.go @@ -57,8 +57,9 @@ func (t *translator) TranslateSSLV2(tls *configv2.ApisixTls) (*apisixv1.Ssl, err return nil, err } ssl.Client = &apisixv1.MutualTLSClientConfig{ - CA: string(ca), - Depth: tls.Spec.Client.Depth, + CA: string(ca), + Depth: tls.Spec.Client.Depth, + SkipMTLSUriRegex: tls.Spec.Client.SkipMTLSUriRegex, } } diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go index d85c386d61..a3695df66a 100644 --- a/pkg/types/apisix/v1/types.go +++ b/pkg/types/apisix/v1/types.go @@ -470,8 +470,9 @@ type Ssl struct { // MutualTLSClientConfig apisix SSL client field // +k8s:deepcopy-gen=true type MutualTLSClientConfig struct { - CA string `json:"ca,omitempty" yaml:"ca,omitempty"` - Depth int `json:"depth,omitempty" yaml:"depth,omitempty"` + CA string `json:"ca,omitempty" yaml:"ca,omitempty"` + Depth int `json:"depth,omitempty" yaml:"depth,omitempty"` + SkipMTLSUriRegex []string `json:"skip_mtls_uri_regex,omitempty" yaml:"skip_mtls_uri_regex, omitempty"` } // StreamRoute represents the stream_route object in APISIX. diff --git a/pkg/types/apisix/v1/zz_generated.deepcopy.go b/pkg/types/apisix/v1/zz_generated.deepcopy.go index df2e82161d..6910b5f421 100644 --- a/pkg/types/apisix/v1/zz_generated.deepcopy.go +++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go @@ -313,6 +313,11 @@ func (in *Metadata) DeepCopy() *Metadata { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MutualTLSClientConfig) DeepCopyInto(out *MutualTLSClientConfig) { *out = *in + if in.SkipMTLSUriRegex != nil { + in, out := &in.SkipMTLSUriRegex, &out.SkipMTLSUriRegex + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -509,7 +514,7 @@ func (in *Ssl) DeepCopyInto(out *Ssl) { if in.Client != nil { in, out := &in.Client, &out.Client *out = new(MutualTLSClientConfig) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/samples/deploy/crd/v1/ApisixTls.yaml b/samples/deploy/crd/v1/ApisixTls.yaml index 731c8cc0fd..a90b4735aa 100644 --- a/samples/deploy/crd/v1/ApisixTls.yaml +++ b/samples/deploy/crd/v1/ApisixTls.yaml @@ -96,6 +96,10 @@ spec: minLength: 1 depth: type: integer + skip_mtls_uri_regex: + type: array + items: + type: string hosts: type: array minItems: 1 diff --git a/test/e2e/scaffold/ssl.go b/test/e2e/scaffold/ssl.go index 00b484d02a..f5e2207b96 100644 --- a/test/e2e/scaffold/ssl.go +++ b/test/e2e/scaffold/ssl.go @@ -88,6 +88,8 @@ spec: name: %s namespace: %s depth: 10 + skip_mtls_uri_regex: + - %s ` ) @@ -137,8 +139,8 @@ func (s *Scaffold) NewApisixTls(name, host, secretName string, ingressClassName } // NewApisixTlsWithClientCA new a ApisixTls CRD -func (s *Scaffold) NewApisixTlsWithClientCA(name, host, secretName, clientCASecret string) error { - tls := fmt.Sprintf(_api6tlsWithClientCATemplate, s.opts.ApisixResourceVersion, name, host, secretName, s.kubectlOptions.Namespace, clientCASecret, s.kubectlOptions.Namespace) +func (s *Scaffold) NewApisixTlsWithClientCA(name, host, secretName, clientCASecret, skipMtlsUriRegex string) error { + tls := fmt.Sprintf(_api6tlsWithClientCATemplate, s.opts.ApisixResourceVersion, name, host, secretName, s.kubectlOptions.Namespace, clientCASecret, s.kubectlOptions.Namespace, skipMtlsUriRegex) if err := s.CreateResourceFromString(tls); err != nil { return err } diff --git a/test/e2e/suite-ingress/suite-ingress-resource/ssl.go b/test/e2e/suite-ingress/suite-ingress-resource/ssl.go index c472ab5726..a292bf6b2b 100644 --- a/test/e2e/suite-ingress/suite-ingress-resource/ssl.go +++ b/test/e2e/suite-ingress/suite-ingress-resource/ssl.go @@ -251,6 +251,48 @@ var _ = ginkgo.Describe("suite-ingress-resource: ApisixTls mTLS Test", func() { suites := func(scaffoldFunc func() *scaffold.Scaffold) { s := scaffoldFunc() + ginkgo.It("create a SSL without client CA and bypass mTLS", func() { + // Without client cert + + // When skip_mtls_uri_regex is set for a route, it won't require a client cert + // update ApisixTls `skip_mtls_uri_regex` + skipMtlsUriRegex := "/ip.*" + tlsName := "tls-with-client-ca" + // create secrets + host := "mtls.httpbin.local" + rootCA, serverCert, serverKey, _, _ := s.GenerateMACert(ginkgo.GinkgoT(), []string{host}) + err := s.NewSecret(serverCertSecret, serverCert.String(), serverKey.String()) + assert.Nil(ginkgo.GinkgoT(), err, "create server cert secret error") + err = s.NewClientCASecret(clientCASecret, rootCA.String(), "") + assert.Nil(ginkgo.GinkgoT(), err, "create client CA cert secret error") + + err = s.NewApisixTlsWithClientCA(tlsName, host, serverCertSecret, clientCASecret, skipMtlsUriRegex) + assert.Nil(ginkgo.GinkgoT(), err, "Update ApisixTls with client CA error") + + // create route + backendSvc, backendSvcPort := s.DefaultHTTPBackend() + apisixRoute := fmt.Sprintf(` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: httpbin-route +spec: + http: + - name: rule1 + match: + hosts: + - mtls.httpbin.local + paths: + - /* + backends: + - serviceName: %s + servicePort: %d +`, backendSvc, backendSvcPort[0]) + assert.Nil(ginkgo.GinkgoT(), s.CreateVersionedApisixResource(apisixRoute)) + assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1)) + + s.NewAPISIXHttpsClient(host).GET("/ip").WithHeader("Host", host).Expect().Status(http.StatusOK) + }) ginkgo.It("create a SSL with client CA", func() { // create secrets host := "mtls.httpbin.local" @@ -263,7 +305,8 @@ var _ = ginkgo.Describe("suite-ingress-resource: ApisixTls mTLS Test", func() { // create ApisixTls resource tlsName := "tls-with-client-ca" - err = s.NewApisixTlsWithClientCA(tlsName, host, serverCertSecret, clientCASecret) + skipMtlsUriRegex := "/unused-route" + err = s.NewApisixTlsWithClientCA(tlsName, host, serverCertSecret, clientCASecret, skipMtlsUriRegex) assert.Nil(ginkgo.GinkgoT(), err, "create ApisixTls with client CA error") // check ssl in APISIX assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixTlsCreated(1)) @@ -290,11 +333,6 @@ spec: assert.Nil(ginkgo.GinkgoT(), s.CreateVersionedApisixResource(apisixRoute)) assert.Nil(ginkgo.GinkgoT(), s.EnsureNumApisixRoutesCreated(1)) - // Without Client Cert - // From APISIX v2.14, If the client does not carry a certificate request, it will fail directly. - // Previous versions would return 400. - // s.NewAPISIXHttpsClient(host).GET("/ip").WithHeader("Host", host).Expect().Status(http.StatusBadRequest).Body().Raw() - // With client cert caCertPool := x509.NewCertPool() ok := caCertPool.AppendCertsFromPEM([]byte(rootCA.String()))