Skip to content

Commit

Permalink
feat: add skip_mtls_uri_regex support for ApisixTls (#1915)
Browse files Browse the repository at this point in the history
* Add `skip_mtls_uri_regex` support


---------

Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
Co-authored-by: Jintao Zhang <zhangjintao9020@gmail.com>
Co-authored-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
  • Loading branch information
3 people committed Dec 20, 2023
1 parent 32396b0 commit 10059af
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 16 deletions.
30 changes: 30 additions & 0 deletions docs/en/latest/concepts/apisix_tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
```
1 change: 1 addition & 0 deletions docs/en/latest/references/apisix_tls_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
50 changes: 50 additions & 0 deletions docs/en/latest/tutorials/mtls-bypass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: MTLS bypass based on regular expression matching against URI
---

<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->

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.*
```
5 changes: 3 additions & 2 deletions pkg/kube/apisix/apis/config/v2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion pkg/kube/apisix/apis/config/v2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions pkg/providers/apisix/translation/apisix_ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/types/apisix/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion pkg/types/apisix/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions samples/deploy/crd/v1/ApisixTls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ spec:
minLength: 1
depth:
type: integer
skip_mtls_uri_regex:
type: array
items:
type: string
hosts:
type: array
minItems: 1
Expand Down
6 changes: 4 additions & 2 deletions test/e2e/scaffold/ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ spec:
name: %s
namespace: %s
depth: 10
skip_mtls_uri_regex:
- %s
`
)

Expand Down Expand Up @@ -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
}
Expand Down
50 changes: 44 additions & 6 deletions test/e2e/suite-ingress/suite-ingress-resource/ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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))
Expand All @@ -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()))
Expand Down

0 comments on commit 10059af

Please sign in to comment.