You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by kv...@apache.org on 2021/05/31 03:32:19 UTC

[apisix-ingress-controller] branch master updated: feat: ApisixTls support mTLS (#492)

This is an automated email from the ASF dual-hosted git repository.

kvn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 38290a2  feat: ApisixTls support mTLS (#492)
38290a2 is described below

commit 38290a2893b4bf77869b34648aeb8d55dd298537
Author: Sarasa Kisaragi <li...@gmail.com>
AuthorDate: Mon May 31 11:32:13 2021 +0800

    feat: ApisixTls support mTLS (#492)
---
 .licenserc.yaml                                    |   2 +
 docs/en/latest/practices/mtls.md                   | 222 ++++++++++++++++
 docs/en/latest/practices/mtls/ca.pem               |  34 +++
 .../en/latest/practices/mtls/client-ca-secret.yaml |  21 ++
 docs/en/latest/practices/mtls/mtls.yaml            |  31 +++
 docs/en/latest/practices/mtls/route.yaml           |  31 +++
 docs/en/latest/practices/mtls/server-secret.yaml   |  23 ++
 docs/en/latest/practices/mtls/server.key           |  51 ++++
 docs/en/latest/practices/mtls/server.pem           |  35 +++
 docs/en/latest/practices/mtls/tls.yaml             |  26 ++
 docs/en/latest/practices/mtls/user.key             |  51 ++++
 docs/en/latest/practices/mtls/user.pem             |  35 +++
 pkg/apisix/ssl.go                                  |   8 +-
 pkg/ingress/apisix_tls.go                          |  26 +-
 pkg/ingress/controller.go                          |   5 +
 pkg/ingress/secret.go                              |  76 +++++-
 pkg/kube/apisix/apis/config/v1/types.go            |  42 ++-
 .../apisix/apis/config/v1/zz_generated.deepcopy.go |  24 +-
 pkg/kube/translation/apisix_ssl.go                 |  22 +-
 pkg/types/apisix/v1/types.go                       |  20 +-
 pkg/types/apisix/v1/zz_generated.deepcopy.go       |  21 ++
 samples/deploy/crd/v1beta1/ApisixTls.yaml          | 149 +++++++++--
 test/e2e/ingress/secret.go                         |  11 +-
 test/e2e/ingress/ssl.go                            | 294 ++++++++++++++++++++-
 test/e2e/scaffold/k8s.go                           |   4 +-
 test/e2e/scaffold/scaffold.go                      |  40 ++-
 test/e2e/scaffold/ssl.go                           |  44 +++
 27 files changed, 1270 insertions(+), 78 deletions(-)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index ad8836c..070b515 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -38,4 +38,6 @@ header:
     - 'pkg/kube/apisix/client/**'
     - '**/zz_generated.deepcopy.go'
     - 'utils/generate-groups.sh'
+    - '**/*.pem'
+    - '**/*.key'
   comment: on-failure
diff --git a/docs/en/latest/practices/mtls.md b/docs/en/latest/practices/mtls.md
new file mode 100644
index 0000000..7651bb4
--- /dev/null
+++ b/docs/en/latest/practices/mtls.md
@@ -0,0 +1,222 @@
+---
+title: Configuring Mutual Authentication via ApisixTls
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+In this practice, we will use mTLS to protect our exposed ingress APIs.
+
+To learn more about mTLS, please refer to [Mutual authentication](https://en.wikipedia.org/wiki/Mutual_authentication)
+
+## Prerequisites
+
+- an available Kubernetes cluster
+- an available APISIX and APISIX Ingress Controller installation
+
+In this guide, we assume that your APISIX is installed in the `apisix` namespace and `ssl` is enabled, which is not enabled by default in the Helm Chart. To enable it, you need to set `gateway.tls.enabled=true` during installation.
+
+Assuming the SSL port is `9443`.
+
+## Deploy httpbin service
+
+We use [kennethreitz/httpbin](https://hub.docker.com/r/kennethreitz/httpbin/) as the service image, See its overview page for details.
+
+Deploy it to the default namespace:
+
+```shell
+kubectl run httpbin --image kennethreitz/httpbin --port 80
+kubectl expose pod httpbin --port 80
+```
+
+## Route the traffic
+
+Since SSL is not configured in ApisixRoute, we can use the config similar to the one in practice [Proxy the httpbin service](./proxy-the-httpbin-service.md).
+
+```yaml
+# route.yaml
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+  name: httpserver-route
+spec:
+  http:
+    - name: httpbin
+      match:
+        hosts:
+          - mtls.httpbin.local
+        paths:
+          - "/*"
+      backend:
+        serviceName: httpbin
+        servicePort: 80
+```
+
+Please remember the host field is `mtls.httpbin.local`. It will be the domain we are going to use.
+
+Test it:
+
+```bash
+kubectl -n apisix exec -it <APISIX_POD_NAME> -- curl "http://127.0.0.1:9080/ip" -H "Host: mtls.httpbin.local"
+```
+
+It should output:
+
+```json
+{
+  "origin": "127.0.0.1"
+}
+```
+
+## Certificates
+
+Before configuring SSL, we must have certificates. Certificates often authorized by certificate provider, which also known as Certification Authority (CA).
+
+You can use [OpenSSL](https://en.wikipedia.org/wiki/Openssl) to generate self-signed certificates for testing purposes. Some pre-generated certificates for this guide are [here](./mtls).
+
+- `ca.pem`: The root CA.
+- `server.pem` and `server.key`: Server certificate used to enable SSL (https). Contains correct `subjectAltName` matches domain `mtls.httpbin.local`.
+- `user.pem` and `user.key`: Client certificate.
+
+To verify them, use commands below:
+
+```bash
+openssl verify -CAfile ./ca.pem ./server.pem
+openssl verify -CAfile ./ca.pem ./user.pem
+```
+
+## Protect the route using SSL
+
+In APISIX Ingress Controller, we use [ApisixTls](../concepts/apisix_tls.md) resource to protect our routes.
+
+ApisixTls requires a secret which field `cert` and `key` contains the certificate and private key.
+
+A secret yaml containing the certificate mentioned above [is here](./mtls/server-secret.yaml). In this guide, we use this as an example.
+
+```bash
+kubectl apply -f ./mtls/server-secret.yaml -n default
+```
+
+The secret name is `server-secret`, we created it in the `default` namespace. We will reference this secret in `ApisixTls`.
+
+```yaml
+# tls.yaml
+apiVersion: apisix.apache.org/v1
+kind: ApisixTls
+metadata:
+  name: sample-tls
+spec:
+  hosts:
+    - mtls.httpbin.local
+  secret:
+    name: server-secret
+    namespace: default
+```
+
+The `secret` field contains the secret reference.
+
+Please note that the `hosts` field matches our domain `mtls.httpbin.local`.
+
+Apply this yaml, APISIX Ingress Controller will use our certificate to protect the route. Let's test it.
+
+```bash
+kubectl -n apisix exec -it <APISIX_POD_NAME> -- curl --resolve 'mtls.httpbin.local:9443:127.0.0.1' "https://mtls.httpbin.local:9443/ip" -k
+```
+
+Some major changes here:
+
+- Use `--resolve` parameter to resolve our domain.
+  - No `Host` header set explicit.
+- We are using `https` and SSL port `9443`.
+- Parameter `-k` to allow insecure connections when using SSL. Because our self-signed certificate is not trusted.
+
+Without the domain `mtls.httpbin.local`, the request won't succeed.
+
+You can add parameter `-v` to log the handshake process.
+
+Now, we configured SSL successfully.
+
+## Mutual Authentication
+
+Like `server-secret`, we will create a `client-ca-secret` to store the CA that verify the certificate client presents.
+
+```bash
+kubectl apply -f ./mtls/client-ca-secret.yaml -n default
+```
+
+Then, change our ApisixTls and apply it:
+
+```yaml
+# mtls.yaml
+apiVersion: apisix.apache.org/v1
+kind: ApisixTls
+metadata:
+  name: sample-tls
+spec:
+  hosts:
+    - mtls.httpbin.local
+  secret:
+    name: server-secret
+    namespace: default
+  client:
+    caSecret:
+      name: client-ca-secret
+      namespace: default
+    depth: 10
+```
+
+The `client` field references the secret, `depth` indicates the max certificate chain length.
+
+Let's try to connect the route without any chanegs:
+
+```bash
+kubectl -n apisix exec -it <APISIX_POD_NAME> -- curl --resolve 'mtls.httpbin.local:9443:127.0.0.1' "https://mtls.httpbin.local:9443/ip" -k
+```
+
+If everything works properly, it will return a `400 Bad Request`.
+
+From APISIX access log, we could find logs like this:
+
+```log
+2021/05/27 17:20:54 [error] 43#43: *106132 [lua] init.lua:293: http_access_phase(): client certificate was not present, client: 127.0.0.1, server: _, request: "GET /ip HTTP/2.0", host: "mtls.httpbin.local:9443"
+127.0.0.1 - - [27/May/2021:17:20:54 +0000] mtls.httpbin.local:9443 "GET /ip HTTP/2.0" 400 154 0.000 "-" "curl/7.76.1" - - - "http://mtls.httpbin.local:9443"
+```
+
+That means our mutual authentication has been enabled successfully.
+
+Now, we need to transfer our client cert to the APISIX container to verify the mTLS functionality.
+
+```bash
+# Transfer client certificate
+kubectl -n apisix cp ./user.key <APISIX_POD_NAME>:/tmp/user.key
+kubectl -n apisix cp ./user.pem <APISIX_POD_NAME>:/tmp/user.pem
+
+# Test
+kubectl -n apisix exec -it <APISIX_POD_NAME> -- curl --resolve 'mtls.httpbin.local:9443:127.0.0.1' "https://mtls.httpbin.local:9443/ip" -k --cert /tmp/user.pem --key /tmp/user.key
+```
+
+Parameter `--cert` and `--key` indicates our certificate and key path.
+
+It should output normally:
+
+```json
+{
+  "origin": "127.0.0.1"
+}
+```
diff --git a/docs/en/latest/practices/mtls/ca.pem b/docs/en/latest/practices/mtls/ca.pem
new file mode 100644
index 0000000..a0c9587
--- /dev/null
+++ b/docs/en/latest/practices/mtls/ca.pem
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF9zCCA9+gAwIBAgIUFKuzAJZgm/fsFS6JDrd+lcpVZr8wDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjI4WhcNMjIwNTI3MTMzNjI4
+WjCBnDELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEYMBYGA1UECgwPQVBJU0lYLVRlc3QtQ0FfMRgwFgYDVQQLDA9BUElT
+SVhfQ0FfUk9PVF8xFTATBgNVBAMMDEFQSVNJWC5ST09UXzEcMBoGCSqGSIb3DQEJ
+ARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+ALJR0lQW/IBqQTE/Oa0Pi4LlmlYUSGnqtFNqiZyOF0PjVzNeqoD9JDPiM1QRyC8p
+NCd5L/QhtUIMMx0RlDI9DkJ3ALIWdrPIZlwpveDJf4KtW7cz+ea46A6QQwB6xcyV
+xWnqEBkiea7qrEE8NakZOMjgkqkN2/9klg6XyA5FWfvszxtuIHtjcy2Kq8bMC0jd
+k7CqEZe4ct6s2wlcI8t8s9prvMDm8gcX66x4Ah+C2/W+C3lTpMDgGqRqSPyCW7na
+Wgn0tWmTSf1iybwYMydhC+zpM1QJLvfDyqjp1wJhziR5ttVe2Xc+tDC24s+u16yZ
+R93IO0M4lLNjvEKJcMltXyRzrcjvLXOhw3KirSHNL1KfrBEl74lb+DV5eU4pIFCj
+cu18gms5FBYs9tpLujwpHDc2MU+zCvRmSPvUA4yCyoXqom3uiSo3g3ymW9IM8dC8
++Bd1GdM6JbpBukvQybc5TQXo1M75I9iEoQa5tQxAfQ/dfwMjOK7skogowBouOuLv
+BEFKy3Vd57IWWZXC4p/74M6N4fGYTgHY5FQE3R4Y2phk/eaEm1jS1UPuC98QuTfL
+rGuFOIBmK5euOm8uT5m9hnrouG2ZcxEdzHYfjsGDGrLzA0FLu+wtMNBKM4NhsNCa
+d+fycLg7jgxWhaLvD5DfkV7WFQlz5LUceYIwYOyhD/chAgMBAAGjLzAtMAwGA1Ud
+EwQFMAMBAf8wHQYDVR0RBBYwFIISbXRscy5odHRwYmluLmxvY2FsMA0GCSqGSIb3
+DQEBCwUAA4ICAQCNtBmoAc5tv3H38sj9qhTmabvp9RIzZYrQSEcN+A2i3a8FVYAM
+YaugZDXDcTycoWn6rcgblUDneow3NiqZ57yYZmN+e4mE3+Q1sGepV7LoRkHDUT8w
+jAJndcZ/xxJmgH6B7dImTAPsvLGR7E7gffMH+aKCdnkG9x5Vm+cuBwSEBndiHGfr
+yw5cXO6cMUq8M6zJrk2V+1BAucXW2rgLTWy6UTTGD56cgUtbStRO6muOKoElDLbW
+mSj2rNv/evakQkV8dgKVRFgh2NQKYKpXmveMaE6xtFFf/dd9OhDFjUh/ksxn94FT
+xj/wkhXCEPl+t7tENhr2tNyLbCOVcFzqoi7IyoWKxxZQfvArfj4SmahK8E/BXB/T
+4PEmn8kZAxaW7RmGcaekm8MTqGlhCJ3tVJAI2vcYRdd9ZHbXE1jr/4xj0I/Lzglo
+O8v5fd4zHyV1SuZ5AH3XbUd7ndl9yDoN2WSqK9Nd9bws3yrf+GwjJAT1InnDvLg1
+stWM8I+9FZiDFL255/+iAN0jYcGu9i4TNvC+o6qQ1p85i1OHPJZu6wtUWMgDJN46
+uwW3ZLh9sZV6OnhbQJBQaUmcgaPJUQqbXNQmpmpc0NUjET/ltFRZ2hlyvvpf7wwF
+2DLY1HRAknQ69DuT6xpYz1aKZqrlkbCWlMMvdosOg6f7+4NxdYJ/rBeS6Q==
+-----END CERTIFICATE-----
diff --git a/docs/en/latest/practices/mtls/client-ca-secret.yaml b/docs/en/latest/practices/mtls/client-ca-secret.yaml
new file mode 100644
index 0000000..fd119ef
--- /dev/null
+++ b/docs/en/latest/practices/mtls/client-ca-secret.yaml
@@ -0,0 +1,21 @@
+# 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.
+apiVersion: v1
+data:
+  cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUY5ekNDQTkrZ0F3SUJBZ0lVRkt1ekFKWmdtL2ZzRlM2SkRyZCtsY3BWWnI4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dad3hDekFKQmdOVkJBWVRBa05PTVJFd0R3WURWUVFJREFoYWFHVnFhV0Z1WnpFUk1BOEdBMVVFQnd3SQpTR0Z1WjNwb2IzVXhHREFXQmdOVkJBb01EMEZRU1ZOSldDMVVaWE4wTFVOQlh6RVlNQllHQTFVRUN3d1BRVkJKClUwbFlYME5CWDFKUFQxUmZNUlV3RXdZRFZRUUREQXhCVUVsVFNWZ3VVazlQVkY4eEhEQWFCZ2txaGtpRzl3MEIKQ1FFV0RYUmxjM1JBZEdWemRDNWpiMjB3SGhjTk1qRXdOVEkzTVRNek5qSTRXaGNOTWpJd05USTNNVE16TmpJNApXakNCbkRFTE1B [...]
+kind: Secret
+metadata:
+  name: client-ca-secret
diff --git a/docs/en/latest/practices/mtls/mtls.yaml b/docs/en/latest/practices/mtls/mtls.yaml
new file mode 100644
index 0000000..da88150
--- /dev/null
+++ b/docs/en/latest/practices/mtls/mtls.yaml
@@ -0,0 +1,31 @@
+# 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.
+
+apiVersion: apisix.apache.org/v1
+kind: ApisixTls
+metadata:
+  name: sample-tls
+spec:
+  hosts:
+    - mtls.httpbin.local
+  secret:
+    name: server-secret
+    namespace: default
+  client:
+    caSecret:
+      name: client-ca-secret
+      namespace: default
+    depth: 10
diff --git a/docs/en/latest/practices/mtls/route.yaml b/docs/en/latest/practices/mtls/route.yaml
new file mode 100644
index 0000000..b50ff4f
--- /dev/null
+++ b/docs/en/latest/practices/mtls/route.yaml
@@ -0,0 +1,31 @@
+# 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.
+
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+  name: httpserver-route
+spec:
+  http:
+    - name: httpbin
+      match:
+        hosts:
+          - mtls.httpbin.local
+        paths:
+          - "/*"
+      backend:
+        serviceName: httpbin
+        servicePort: 80
diff --git a/docs/en/latest/practices/mtls/server-secret.yaml b/docs/en/latest/practices/mtls/server-secret.yaml
new file mode 100644
index 0000000..bfbedd8
--- /dev/null
+++ b/docs/en/latest/practices/mtls/server-secret.yaml
@@ -0,0 +1,23 @@
+# 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.
+
+apiVersion: v1
+data:
+  cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUYvVENDQStXZ0F3SUJBZ0lVQmJVUDdHazBXQWIvSmhZWWNCQmdaRWdtaGJFd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dad3hDekFKQmdOVkJBWVRBa05PTVJFd0R3WURWUVFJREFoYWFHVnFhV0Z1WnpFUk1BOEdBMVVFQnd3SQpTR0Z1WjNwb2IzVXhHREFXQmdOVkJBb01EMEZRU1ZOSldDMVVaWE4wTFVOQlh6RVlNQllHQTFVRUN3d1BRVkJKClUwbFlYME5CWDFKUFQxUmZNUlV3RXdZRFZRUUREQXhCVUVsVFNWZ3VVazlQVkY4eEhEQWFCZ2txaGtpRzl3MEIKQ1FFV0RYUmxjM1JBZEdWemRDNWpiMjB3SGhjTk1qRXdOVEkzTVRNek5qSTVXaGNOTWpJd05USTNNVE16TmpJNQpXakNCcFRFTE1B [...]
+  key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBeGxFOGJ5QlNzNFl6aHJDZFhvUHdPelJkdnFOVnVJYVRIN1ZpeTgvSG1nZ1RnQ3pBCm5TWExyT3FFRVdlbENqTVVicmNwK3dJRHBUZnI4TzNMZXNoc25PeHM3dGhvNHdraTJpSkNDcDJvWGFldVkrbWEKa0pDNHNZcHBXK3VKRUlQbmswU1lWQSt5R1ZGOXhUbjhRU3Q0MHB0Rzk3Zk1Rb2RHa0lNRm5ZeksrdW0zY0lKWApMb014c3VXVnVOUzlwNTJ1ZERHV1lqbDN2SGRRSjdnUzZlcnkrZnR6U25oK3NEV2Z4UEZ0ZlF6aGl2MkRkZ1FTCm9LOURmLzJOVGlFamtLKzZNS242N3YwUnE4bGwreG9TL2RGaUFlU2dTSHVyNDRTUlJxTlpjcVBoYktlTE90cGEKd2UvNHU4c [...]
+kind: Secret
+metadata:
+  name: server-secret
diff --git a/docs/en/latest/practices/mtls/server.key b/docs/en/latest/practices/mtls/server.key
new file mode 100644
index 0000000..6c0e3d4
--- /dev/null
+++ b/docs/en/latest/practices/mtls/server.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/docs/en/latest/practices/mtls/server.pem b/docs/en/latest/practices/mtls/server.pem
new file mode 100644
index 0000000..b253f81
--- /dev/null
+++ b/docs/en/latest/practices/mtls/server.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIF/TCCA+WgAwIBAgIUBbUP7Gk0WAb/JhYYcBBgZEgmhbEwDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjI5WhcNMjIwNTI3MTMzNjI5
+WjCBpTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEcMBoGA1UECgwTQVBJU0lYLVRlc3QtU2VydmVyXzEXMBUGA1UECwwO
+QVBJU0lYX1NFUlZFUl8xGzAZBgNVBAMMEm10bHMuaHR0cGJpbi5sb2NhbDEcMBoG
+CSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAMZRPG8gUrOGM4awnV6D8Ds0Xb6jVbiGkx+1YsvPx5oIE4AswJ0l
+y6zqhBFnpQozFG63KfsCA6U36/Dty3rIbJzsbO7YaOMJItoiQgqdqF2nrmPpmpCQ
+uLGKaVvriRCD55NEmFQPshlRfcU5/EEreNKbRve3zEKHRpCDBZ2Myvrpt3CCVy6D
+MbLllbjUvaedrnQxlmI5d7x3UCe4Eunq8vn7c0p4frA1n8TxbX0M4Yr9g3YEEqCv
+Q3/9jU4hI5CvujCp+u79EavJZfsaEv3RYgHkoEh7q+OEkUajWXKj4WynizraWsHv
++LvK9pfI300p1HSKK4FqonvW79anRNbK+8BqV4Wt5aBeFU/rW2jHtJxcl1OLRrrh
+wftCP5W7vSjvJes5wPDZjDEyv8WP1Aa6yWeGHHtIwrAHPr7556F/JAQS6IPBQQ5U
+X45DD2aNXME9xZKdBtyMovItjZm31UUsvoF+YtpAOmbEkX4lMznUO3XZJjM5HWSq
+WYyzmFsw+pJEwhXRo4GfSfCHfiZQ6imTLJ7OsZzo9bvmxyfI0kVLe3h3iCe+qYeT
+f5AJU6v5vv3thCtfgbxYP2P8b+0MIrfr05e6dCDXbIri1z+nprzWYmyCrZ6H4hVk
+DzMktFUlkqenvnsJ2iOV2AZw0Hlk2bwe4zSumzqoIp8Yk/kxbfxhQqr5AgMBAAGj
+LDAqMAkGA1UdEwQCMAAwHQYDVR0RBBYwFIISbXRscy5odHRwYmluLmxvY2FsMA0G
+CSqGSIb3DQEBCwUAA4ICAQCDDfETCEpWB/KRQZo2JF8n4NEDTeraQ85M3H5luJHp
+NdJO4oYq3n8B149ep4FcEYdO20pV+TMeMNWXMfhoRIpGx95JrLuLg6qnw6eNdErn
+YupHMC2OEoEWVcmI052LDJcXuKsTXQvU4OeEL2dX4OtNJ+mRODLyh40cg7dA3wry
+kGLiprRlLQtiX8pSDG30qPZexL1LcFzBQajriG05QUrJW6Rvbq1JTIlyp7E1T86f
+Xljq0Hdzqxy+FklYcAW5ZAxgkQlMmVdTlvDXlD/hQLEQIHGHiW6OMLp8WrnJP6b0
+D2HqWmOwuEzqSgXSK0N89rpiWP1FKCpyiKVcsawDNfOpePVuthommVEc2PxacyHf
+UCC9V0MS0ZzQ63Tnz2Tja8C6/kMyVX226KQKhcoDxDoS0mQrI96/VXcglwP5hMjF
+joth1T1qRVu6+NQmvFPaNjbzWJ+j1R99bnYGihPeLdqDSUxNosV3ULG8T4aN6+f8
+hApiqg2dkLJQr8zWf6vWXMlREdPEovb2F7P0Lfn0VeOSRXDUIdqcoRHONi8bWMRs
+fjPtGW00Tv8Jg21c9vc8Zh/t1w3wkXQhqYiBMt5cYe6WueIlXdjF7ikSRWAHTwlw
+Bfzv/vMftLnbySPovCzQ1PF55D01EWRk0o6PRwUDLfzTQoV+bDKx82LxKtZBtQEX
+uw==
+-----END CERTIFICATE-----
diff --git a/docs/en/latest/practices/mtls/tls.yaml b/docs/en/latest/practices/mtls/tls.yaml
new file mode 100644
index 0000000..bda748a
--- /dev/null
+++ b/docs/en/latest/practices/mtls/tls.yaml
@@ -0,0 +1,26 @@
+# 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.
+
+apiVersion: apisix.apache.org/v1
+kind: ApisixTls
+metadata:
+  name: sample-tls
+spec:
+  hosts:
+    - mtls.httpbin.local
+  secret:
+    name: server-secret
+    namespace: default
diff --git a/docs/en/latest/practices/mtls/user.key b/docs/en/latest/practices/mtls/user.key
new file mode 100644
index 0000000..5e0feba
--- /dev/null
+++ b/docs/en/latest/practices/mtls/user.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/docs/en/latest/practices/mtls/user.pem b/docs/en/latest/practices/mtls/user.pem
new file mode 100644
index 0000000..a6b3746
--- /dev/null
+++ b/docs/en/latest/practices/mtls/user.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGDDCCA/SgAwIBAgIUBbUP7Gk0WAb/JhYYcBBgZEgmhbIwDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjMxWhcNMjIwNTI3MTMzNjMx
+WjCBtDELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEfMB0GA1UECgwWQVBJU0lYLVRlc3QtUm9vdC1Vc2VyXzEaMBgGA1UE
+CwwRQVBJU0lYX1JPT1RfVVNFUl8xJDAiBgNVBAMMG0JpZ01pbmdfIDY1NDMxMTEx
+MTExMTExMTExMTEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL41i+W8fgqtKButnkOa0qGApPJ7HSUI
+gxBt6Tb7mR8eka8vno8hIE42hwMXYoIiO1FgM9M9fPN8vzTnWr2iTAXbw/qXdn14
+RT01JrGzj1OU3M9U0RMmDWVs8VOY3ncdcdPoqctMNr2K+6wlN6EpG1DQDUEvB0Yy
+qZpFWpuZP0akzjFqeJNSo3b8hP4J1nsWBxKt6TRInC60jT1mhKCjTB1IsXVC+5SY
+f0FhMN2ZQPgCyIIq+j5S7tHta4Qs+UDNY0whIMBkY3ZAGPzUBFExwrX7bGNbeJYz
+DahNAKiEj5dlieRvzTn5UisSv5EzDUONLSluEnCU1Ghd/dfZtZwlMBU6OIflwm6W
+X7HePD0G8XEDe5qbU/tg/62j+A1hMS2gSlibNisyvNwMNz6Imc3CnIFUL2mTsgl1
+bCBLfOXDfrjte5Txnt26ayN0+WhmVSts0+ln6vnGtgVGqH0bmp95yMRYPej+6ARE
+MtfvrNA7iZORypspv2NVyV11b6Jx3o/pJOAq9dmAsKvJ6hK0IVf/55t3ZtzRa/fY
+w0f78KbWuEc7qmx2tBxnEfppb5iB/RFmI2bDaAEA0lDQgAqmiAu1utetfqsCwA1m
+3VXGp8Vp+lexA/M3Bkb5BHa6C11QDHvGjUVwduOymLAHKumvdgf7TJFnlhpg6jYN
+unO58bRSTI6LAgMBAAGjLDAqMAkGA1UdEwQCMAAwHQYDVR0RBBYwFIISbXRscy5o
+dHRwYmluLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4ICAQCoTBvw11aKah2cuB4XplUB
+nmkrWhfLNFJLVrU9jIISP9Wkl3s3PcM+aEWygb/1roqbMqNOgrzDVgGRRFCiS6qi
+himUuTJhIBI6TF1PE+XW3gTFWBXkAZ7MzpbS8oP1PehlY3XXKNZgxZi3XaDI8Hfw
+5MWBGNbk8tegn8bvYQUz2VxmCo6zufCkj4ADjw2zhiyKBKuHTzg48w66Wn4jLhlK
+p91HHrK0lEOIJ4pFmBUpBsSJMlBMbfrzBF87xQhpDO3ScFfCWUatShmXsPMJU0F0
+DEuTnaHUefUf/F9wUGNcA4yQ4pH8SxVpRHmrWE8U4uSXpz1bx4ChZurJ9mPzrj9h
+U9c/d9F5WndZNPcR1R8Tbzhk/R8GImVR3Lt59cW+SN/+4JVFy+Hye2yslGFn2CAJ
+ofNxjLb9OE6+EE3SWW0B5CZSWBS49gtdTW0ApOjIRJU2zipxcjnNf00GFoIoCxjk
+Z4eBQz9WVUM9KSrJIQSLZQd35tZAOp0BuwWho0+w8lXUchSqT7oRA7+szZldWF0j
+HKPIMJ0iVWmXuZjsS8q8NBIt4DuBcqpevlol5KRXv6tJy4IBVAVEIBdeXotvdxKE
+bncvZ6xo9A/waUU7tEyzv34usxefrWxtSlOA1G0Jj4nb5gKPHjn0XIr9WI2RpovT
+/XpB6QES1zoBQya3QjnDbQ==
+-----END CERTIFICATE-----
diff --git a/pkg/apisix/ssl.go b/pkg/apisix/ssl.go
index 16a9f7c..257117b 100644
--- a/pkg/apisix/ssl.go
+++ b/pkg/apisix/ssl.go
@@ -141,13 +141,7 @@ func (s *sslClient) Create(ctx context.Context, obj *v1.Ssl) (*v1.Ssl, error) {
 	if err := s.cluster.HasSynced(ctx); err != nil {
 		return nil, err
 	}
-	data, err := json.Marshal(v1.Ssl{
-		ID:     obj.ID,
-		Snis:   obj.Snis,
-		Cert:   obj.Cert,
-		Key:    obj.Key,
-		Status: obj.Status,
-	})
+	data, err := json.Marshal(obj)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/ingress/apisix_tls.go b/pkg/ingress/apisix_tls.go
index 643d37b..2ba036d 100644
--- a/pkg/ingress/apisix_tls.go
+++ b/pkg/ingress/apisix_tls.go
@@ -123,13 +123,19 @@ func (c *apisixTlsController) sync(ctx context.Context, ev *types.Event) error {
 		c.controller.recordStatus(tls, _resourceSyncAborted, err, metav1.ConditionFalse)
 		return err
 	}
-	log.Debug("got SSL object from ApisixTls",
+	log.Debugw("got SSL object from ApisixTls",
 		zap.Any("ssl", ssl),
 		zap.Any("ApisixTls", tls),
 	)
 
 	secretKey := tls.Spec.Secret.Namespace + "_" + tls.Spec.Secret.Name
-	c.syncSecretSSL(secretKey, ssl, ev.Type)
+	c.syncSecretSSL(secretKey, key, ssl, ev.Type)
+	if tls.Spec.Client != nil {
+		caSecretKey := tls.Spec.Client.CASecret.Namespace + "_" + tls.Spec.Client.CASecret.Name
+		if caSecretKey != secretKey {
+			c.syncSecretSSL(caSecretKey, key, ssl, ev.Type)
+		}
+	}
 
 	if err := c.controller.syncSSL(ctx, ssl, ev.Type); err != nil {
 		log.Errorw("failed to sync SSL to APISIX",
@@ -146,21 +152,21 @@ func (c *apisixTlsController) sync(ctx context.Context, ev *types.Event) error {
 	return err
 }
 
-func (c *apisixTlsController) syncSecretSSL(key string, ssl *v1.Ssl, event types.EventType) {
-	if ssls, ok := c.controller.secretSSLMap.Load(key); ok {
+func (c *apisixTlsController) syncSecretSSL(secretKey string, apisixTlsKey string, ssl *v1.Ssl, event types.EventType) {
+	if ssls, ok := c.controller.secretSSLMap.Load(secretKey); ok {
 		sslMap := ssls.(*sync.Map)
 		switch event {
 		case types.EventDelete:
-			sslMap.Delete(ssl.ID)
-			c.controller.secretSSLMap.Store(key, sslMap)
+			sslMap.Delete(apisixTlsKey)
+			c.controller.secretSSLMap.Store(secretKey, sslMap)
 		default:
-			sslMap.Store(ssl.ID, ssl)
-			c.controller.secretSSLMap.Store(key, sslMap)
+			sslMap.Store(apisixTlsKey, ssl)
+			c.controller.secretSSLMap.Store(secretKey, sslMap)
 		}
 	} else if event != types.EventDelete {
 		sslMap := new(sync.Map)
-		sslMap.Store(ssl.ID, ssl)
-		c.controller.secretSSLMap.Store(key, sslMap)
+		sslMap.Store(apisixTlsKey, ssl)
+		c.controller.secretSSLMap.Store(secretKey, sslMap)
 	}
 }
 
diff --git a/pkg/ingress/controller.go b/pkg/ingress/controller.go
index 4568155..04ecfe1 100644
--- a/pkg/ingress/controller.go
+++ b/pkg/ingress/controller.go
@@ -246,6 +246,11 @@ func (c *Controller) recorderEvent(object runtime.Object, eventtype, reason stri
 	}
 }
 
+// recorderEvent recorder events for resources
+func (c *Controller) recorderEventS(object runtime.Object, eventtype, reason string, msg string) {
+	c.recorder.Event(object, eventtype, reason, msg)
+}
+
 func (c *Controller) goAttach(handler func()) {
 	c.wg.Add(1)
 	go func() {
diff --git a/pkg/ingress/secret.go b/pkg/ingress/secret.go
index 8b3aec2..2d8e763 100644
--- a/pkg/ingress/secret.go
+++ b/pkg/ingress/secret.go
@@ -17,12 +17,14 @@ package ingress
 
 import (
 	"context"
+	"fmt"
 	"sync"
 	"time"
 
 	"go.uber.org/zap"
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/workqueue"
 
@@ -130,21 +132,62 @@ func (c *secretController) sync(ctx context.Context, ev *types.Event) error {
 		// This secret is not concerned.
 		return nil
 	}
-	cert, ok := sec.Data["cert"]
-	if !ok {
-		return translation.ErrEmptyCert
-	}
-	pkey, ok := sec.Data["key"]
-	if !ok {
-		return translation.ErrEmptyPrivKey
-	}
 	sslMap := ssls.(*sync.Map)
-	sslMap.Range(func(_, v interface{}) bool {
+	sslMap.Range(func(k, v interface{}) bool {
 		ssl := v.(*apisixv1.Ssl)
-		// sync ssl
-		ssl.Cert = string(cert)
-		ssl.Key = string(pkey)
-
+		tlsMetaKey := k.(string)
+		tlsNamespace, tlsName, err := cache.SplitMetaNamespaceKey(tlsMetaKey)
+		if err != nil {
+			log.Errorf("invalid cached ApisixTls key: %s", tlsMetaKey)
+			return true
+		}
+		tls, err := c.controller.apisixTlsLister.ApisixTlses(tlsNamespace).Get(tlsName)
+		if err != nil {
+			log.Warnw("secret related ApisixTls resource not found, skip",
+				zap.String("ApisixTls", tlsMetaKey),
+			)
+			return true
+		}
+		if tls.Spec.Secret.Namespace == sec.Namespace && tls.Spec.Secret.Name == sec.Name {
+			cert, ok := sec.Data["cert"]
+			if !ok {
+				log.Warnw("secret required by ApisixTls invalid",
+					zap.String("ApisixTls", tlsMetaKey),
+					zap.Error(translation.ErrEmptyCert),
+				)
+				return true
+			}
+			pkey, ok := sec.Data["key"]
+			if !ok {
+				log.Warnw("secret required by ApisixTls invalid",
+					zap.String("ApisixTls", tlsMetaKey),
+					zap.Error(translation.ErrEmptyPrivKey),
+				)
+				return true
+			}
+			// sync ssl
+			ssl.Cert = string(cert)
+			ssl.Key = string(pkey)
+		} else if tls.Spec.Client != nil &&
+			tls.Spec.Client.CASecret.Namespace == sec.Namespace && tls.Spec.Client.CASecret.Name == sec.Name {
+			ca, ok := sec.Data["cert"]
+			if !ok {
+				log.Warnw("secret required by ApisixTls invalid",
+					zap.String("resource", tlsMetaKey),
+					zap.Error(translation.ErrEmptyCert),
+				)
+				return true
+			}
+			ssl.Client = &apisixv1.MutualTLSClientConfig{
+				CA: string(ca),
+			}
+		} else {
+			log.Warnw("stale secret cache, ApisixTls doesn't requires target secret",
+				zap.String("ApisixTls", tlsMetaKey),
+				zap.String("secret", key),
+			)
+			return true
+		}
 		// Use another goroutine to send requests, to avoid
 		// long time lock occupying.
 		go func(ssl *apisixv1.Ssl) {
@@ -155,6 +198,13 @@ func (c *secretController) sync(ctx context.Context, ev *types.Event) error {
 					zap.Any("ssl", ssl),
 					zap.Any("secret", sec),
 				)
+				c.controller.recorderEventS(tls, corev1.EventTypeWarning, _resourceSyncAborted,
+					fmt.Sprintf("sync from secret %s changes failed, error: %s", key, err.Error()))
+				c.controller.recordStatus(tls, _resourceSyncAborted, err, metav1.ConditionFalse)
+			} else {
+				c.controller.recorderEventS(tls, corev1.EventTypeNormal, _resourceSynced,
+					fmt.Sprintf("sync from secret %s changes", key))
+				c.controller.recordStatus(tls, _resourceSynced, nil, metav1.ConditionTrue)
 			}
 		}(ssl)
 		return true
diff --git a/pkg/kube/apisix/apis/config/v1/types.go b/pkg/kube/apisix/apis/config/v1/types.go
index 72a4e2c..a8b9181 100644
--- a/pkg/kube/apisix/apis/config/v1/types.go
+++ b/pkg/kube/apisix/apis/config/v1/types.go
@@ -29,6 +29,8 @@ import (
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 // ApisixRoute is used to define the route rules and upstreams for Apache APISIX.
 // The definition closes the Kubernetes Ingress resource.
+// +kubebuilder:resource:shortName=ar
+// +kubebuilder:pruning:PreserveUnknownFields
 type ApisixRoute struct {
 	metav1.TypeMeta   `json:",inline" yaml:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
@@ -268,30 +270,58 @@ func (p *Config) DeepCopy() *Config {
 
 // +genclient
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:shortName=atls
 // +kubebuilder:subresource:status
 // ApisixTls defines SSL resource in APISIX.
 type ApisixTls struct {
 	metav1.TypeMeta   `json:",inline" yaml:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
-	Spec              *ApisixTlsSpec        `json:"spec,omitempty" yaml:"spec,omitempty"`
-	Status            v2alpha1.ApisixStatus `json:"status,omitempty" yaml:"status,omitempty"`
+	Spec              *ApisixTlsSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
+	// +optional
+	Status v2alpha1.ApisixStatus `json:"status,omitempty" yaml:"status,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:printcolumn:name="SNIs",type=string,JSONPath=`.spec.hosts`
+// +kubebuilder:printcolumn:name="Secret Name",type=string,JSONPath=`.spec.secret.name`
+// +kubebuilder:printcolumn:name="Secret Namespace",type=string,JSONPath=`.spec.secret.namespace`
+// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
+// +kubebuilder:printcolumn:name="Client CA Secret Name",type=string,JSONPath=`.spec.client.ca.name`
+// +kubebuilder:printcolumn:name="Client CA Secret Namespace",type=string,JSONPath=`.spec.client.ca.namespace`
 type ApisixTlsList struct {
 	metav1.TypeMeta `json:",inline" yaml:",inline"`
 	metav1.ListMeta `json:"metadata" yaml:"metadata"`
 	Items           []ApisixTls `json:"items,omitempty" yaml:"items,omitempty"`
 }
 
+// +kubebuilder:validation:Pattern="^\\*?[0-9a-zA-Z-.]+$"
+type HostType string
+
 // ApisixTlsSpec is the specification of ApisixSSL.
 type ApisixTlsSpec struct {
-	Hosts  []string     `json:"hosts,omitempty" yaml:"hosts,omitempty"`
-	Secret ApisixSecret `json:"secret,omitempty" yaml:"secret,omitempty"`
+	// +required
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinItems=1
+	Hosts []HostType `json:"hosts" yaml:"hosts,omitempty"`
+	// +required
+	// +kubebuilder:validation:Required
+	Secret ApisixSecret `json:"secret" yaml:"secret"`
+	// +optional
+	Client *ApisixMutualTlsClientConfig `json:"client,omitempty" yaml:"client,omitempty"`
 }
 
 // ApisixSecret describes the Kubernetes Secret name and namespace.
 type ApisixSecret struct {
-	Name      string `json:"name,omitempty" yaml:"name,omitempty"`
-	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:Required
+	Name string `json:"name" yaml:"name"`
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:Required
+	Namespace string `json:"namespace" yaml:"namespace"`
+}
+
+// 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"`
 }
diff --git a/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go b/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
index 1927a84..c0f4d32 100644
--- a/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
+++ b/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
@@ -96,6 +96,23 @@ func (in *ActiveHealthCheckUnhealthy) DeepCopy() *ActiveHealthCheckUnhealthy {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApisixMutualTlsClientConfig) DeepCopyInto(out *ApisixMutualTlsClientConfig) {
+	*out = *in
+	out.CASecret = in.CASecret
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixMutualTlsClientConfig.
+func (in *ApisixMutualTlsClientConfig) DeepCopy() *ApisixMutualTlsClientConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(ApisixMutualTlsClientConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -268,10 +285,15 @@ func (in *ApisixTlsSpec) DeepCopyInto(out *ApisixTlsSpec) {
 	*out = *in
 	if in.Hosts != nil {
 		in, out := &in.Hosts, &out.Hosts
-		*out = make([]string, len(*in))
+		*out = make([]HostType, len(*in))
 		copy(*out, *in)
 	}
 	out.Secret = in.Secret
+	if in.Client != nil {
+		in, out := &in.Client, &out.Client
+		*out = new(ApisixMutualTlsClientConfig)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/kube/translation/apisix_ssl.go b/pkg/kube/translation/apisix_ssl.go
index 3dd4f30..7ab54fa 100644
--- a/pkg/kube/translation/apisix_ssl.go
+++ b/pkg/kube/translation/apisix_ssl.go
@@ -19,7 +19,6 @@ import (
 
 	"github.com/apache/apisix-ingress-controller/pkg/id"
 	configv1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
-	apisix "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
 	apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
 )
 
@@ -44,8 +43,10 @@ func (t *translator) TranslateSSL(tls *configv1.ApisixTls) (*apisixv1.Ssl, error
 		return nil, ErrEmptyPrivKey
 	}
 	var snis []string
-	snis = append(snis, tls.Spec.Hosts...)
-	ssl := &apisix.Ssl{
+	for _, host := range tls.Spec.Hosts {
+		snis = append(snis, string(host))
+	}
+	ssl := &apisixv1.Ssl{
 		ID:     id.GenID(tls.Namespace + "_" + tls.Name),
 		Snis:   snis,
 		Cert:   string(cert),
@@ -55,5 +56,20 @@ func (t *translator) TranslateSSL(tls *configv1.ApisixTls) (*apisixv1.Ssl, error
 			"managed-by": "apisix-ingress-controller",
 		},
 	}
+	if tls.Spec.Client != nil {
+		caSecret, err := t.SecretLister.Secrets(tls.Spec.Client.CASecret.Namespace).Get(tls.Spec.Client.CASecret.Name)
+		if err != nil {
+			return nil, err
+		}
+		ca, ok := caSecret.Data["cert"]
+		if !ok {
+			return nil, ErrEmptyCert
+		}
+		ssl.Client = &apisixv1.MutualTLSClientConfig{
+			CA:    string(ca),
+			Depth: tls.Spec.Client.Depth,
+		}
+	}
+
 	return ssl, nil
 }
diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go
index 393b461..467872e 100644
--- a/pkg/types/apisix/v1/types.go
+++ b/pkg/types/apisix/v1/types.go
@@ -295,12 +295,20 @@ type UpstreamPassiveHealthCheckUnhealthy struct {
 // Ssl apisix ssl object
 // +k8s:deepcopy-gen=true
 type Ssl struct {
-	ID     string            `json:"id,omitempty" yaml:"id,omitempty"`
-	Snis   []string          `json:"snis,omitempty" yaml:"snis,omitempty"`
-	Cert   string            `json:"cert,omitempty" yaml:"cert,omitempty"`
-	Key    string            `json:"key,omitempty" yaml:"key,omitempty"`
-	Status int               `json:"status,omitempty" yaml:"status,omitempty"`
-	Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
+	ID     string                 `json:"id,omitempty" yaml:"id,omitempty"`
+	Snis   []string               `json:"snis,omitempty" yaml:"snis,omitempty"`
+	Cert   string                 `json:"cert,omitempty" yaml:"cert,omitempty"`
+	Key    string                 `json:"key,omitempty" yaml:"key,omitempty"`
+	Status int                    `json:"status,omitempty" yaml:"status,omitempty"`
+	Labels map[string]string      `json:"labels,omitempty" yaml:"labels,omitempty"`
+	Client *MutualTLSClientConfig `json:"client,omitempty" yaml:"client,omitempty"`
+}
+
+// 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"`
 }
 
 // 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 911eb86..64fcdba 100644
--- a/pkg/types/apisix/v1/zz_generated.deepcopy.go
+++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go
@@ -174,6 +174,22 @@ 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
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutualTLSClientConfig.
+func (in *MutualTLSClientConfig) DeepCopy() *MutualTLSClientConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(MutualTLSClientConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RedirectConfig) DeepCopyInto(out *RedirectConfig) {
 	*out = *in
 	return
@@ -276,6 +292,11 @@ func (in *Ssl) DeepCopyInto(out *Ssl) {
 			(*out)[key] = val
 		}
 	}
+	if in.Client != nil {
+		in, out := &in.Client, &out.Client
+		*out = new(MutualTLSClientConfig)
+		**out = **in
+	}
 	return
 }
 
diff --git a/samples/deploy/crd/v1beta1/ApisixTls.yaml b/samples/deploy/crd/v1beta1/ApisixTls.yaml
index b60eea7..c4e1858 100644
--- a/samples/deploy/crd/v1beta1/ApisixTls.yaml
+++ b/samples/deploy/crd/v1beta1/ApisixTls.yaml
@@ -21,23 +21,23 @@ metadata:
   name: apisixtlses.apisix.apache.org
 spec:
   additionalPrinterColumns:
-    - JSONPath: .spec.hosts
-      name: SNIs
-      type: string
-    - JSONPath: .spec.secret.name
-      name: Secret Name
-      type: string
-    - JSONPath: .spec.secret.namespace
-      name: Secret Namespace
-      type: string
-    - JSONPath: .metadata.creationTimestamp
-      name: Age
-      type: date
+  - JSONPath: .spec.hosts
+    name: SNIs
+    type: string
+  - JSONPath: .spec.secret.name
+    name: Secret Name
+    type: string
+  - JSONPath: .spec.secret.namespace
+    name: Secret Namespace
+    type: string
+  - JSONPath: .metadata.creationTimestamp
+    name: Age
+    type: date
   group: apisix.apache.org
   versions:
-    - name: v1
-      served: true
-      storage: true
+  - name: v1
+    served: true
+    storage: true
   scope: Namespaced
   names:
     plural: apisixtlses
@@ -50,25 +50,61 @@ spec:
     status: {}
   validation:
     openAPIV3Schema:
+      description: ApisixTls defines SSL resource in APISIX.
       type: object
       properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
         spec:
+          description: ApisixTlsSpec is the specification of ApisixSSL.
           type: object
           required:
-            - hosts
-            - secret
+          - hosts
+          - secret
           properties:
+            client:
+              description: ApisixMutualTlsClientConfig describes the mutual TLS CA
+                and verify depth
+              type: object
+              properties:
+                caSecret:
+                  description: ApisixSecret describes the Kubernetes Secret name and
+                    namespace.
+                  type: object
+                  required:
+                  - name
+                  - namespace
+                  properties:
+                    name:
+                      type: string
+                      minLength: 1
+                    namespace:
+                      type: string
+                      minLength: 1
+                depth:
+                  type: integer
             hosts:
               type: array
               minItems: 1
               items:
                 type: string
-                pattern: "^\\*?[0-9a-zA-Z-.]+$"
+                pattern: ^\*?[0-9a-zA-Z-.]+$
             secret:
+              description: ApisixSecret describes the Kubernetes Secret name and namespace.
               type: object
               required:
-                - name
-                - namespace
+              - name
+              - namespace
               properties:
                 name:
                   type: string
@@ -76,3 +112,76 @@ spec:
                 namespace:
                   type: string
                   minLength: 1
+        status:
+          description: ApisixStatus is the status report for Apisix ingress Resources
+          type: object
+          properties:
+            conditions:
+              type: array
+              items:
+                description: "Condition contains details for one aspect of the current
+                  state of this API Resource. --- This struct is intended for direct
+                  use as an array at the field path .status.conditions.  For example,
+                  type FooStatus struct{     // Represents the observations of a foo's
+                  current state.     // Known .status.conditions.type are: \"Available\",
+                  \"Progressing\", and \"Degraded\"     // +patchMergeKey=type     //
+                  +patchStrategy=merge     // +listType=map     // +listMapKey=type
+                  \    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                  patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
+                  \n     // other fields }"
+                type: object
+                required:
+                - lastTransitionTime
+                - message
+                - reason
+                - status
+                - type
+                properties:
+                  lastTransitionTime:
+                    description: lastTransitionTime is the last time the condition
+                      transitioned from one status to another. This should be when
+                      the underlying condition changed.  If that is not known, then
+                      using the time when the API field changed is acceptable.
+                    type: string
+                    format: date-time
+                  message:
+                    description: message is a human readable message indicating details
+                      about the transition. This may be an empty string.
+                    type: string
+                    maxLength: 32768
+                  observedGeneration:
+                    description: observedGeneration represents the .metadata.generation
+                      that the condition was set based upon. For instance, if .metadata.generation
+                      is currently 12, but the .status.conditions[x].observedGeneration
+                      is 9, the condition is out of date with respect to the current
+                      state of the instance.
+                    type: integer
+                    format: int64
+                    minimum: 0
+                  reason:
+                    description: reason contains a programmatic identifier indicating
+                      the reason for the condition's last transition. Producers of
+                      specific condition types may define expected values and meanings
+                      for this field, and whether the values are considered a guaranteed
+                      API. The value should be a CamelCase string. This field may
+                      not be empty.
+                    type: string
+                    maxLength: 1024
+                    minLength: 1
+                    pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                  status:
+                    description: status of the condition, one of True, False, Unknown.
+                    type: string
+                    enum:
+                    - "True"
+                    - "False"
+                    - Unknown
+                  type:
+                    description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      --- Many .condition.type values are consistent across resources
+                      like Available, but because arbitrary conditions can be useful
+                      (see .node.status.conditions), the ability to deconflict is
+                      important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                    type: string
+                    maxLength: 316
+                    pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
diff --git a/test/e2e/ingress/secret.go b/test/e2e/ingress/secret.go
index 2c19594..421a6a2 100644
--- a/test/e2e/ingress/secret.go
+++ b/test/e2e/ingress/secret.go
@@ -26,6 +26,7 @@ import (
 	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
 )
 
+// TODO: FIXME
 var _ = ginkgo.Describe("secret Testing", func() {
 	opts := &scaffold.Options{
 		Name:                  "default",
@@ -153,7 +154,7 @@ jW4KB95bGOTa7r7DM1Up0MbAIwWoeLBGhOIXk7inurZGg+FNjZMA5Lzm6qo=
 		assert.Nil(ginkgo.GinkgoT(), err, "create tls error")
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err := s.ListApisixTls()
+		tls, err := s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 1, "tls number not expect")
 		assert.Equal(ginkgo.GinkgoT(), cert, tls[0].Cert, "tls cert not expect")
@@ -245,17 +246,17 @@ UnBVSIGJ/c0AhVSDuOAJiF36pvsDysTZXMTFE/9i5bkGOiwtzRNe4Hym/SEZUCpn
 8Z1U5/a5LhrDtIKAsCCpRx99P++Eqt2M2YV7jZcTfEbEvxP4XBYcdh30nbq1uEhs
 4zEnK1pMx5PnEljN1mcgmL2TPsMVN5DN9zXHW5eNQ6wfXR8rCfHwVIVcUuaB
 -----END RSA PRIVATE KEY-----`
-		key_compare_update := "HrMHUvE9Esvn7GnZ+vAynaIg/8wlB3r0zm0htmnwofY0a95jf9O5bkBT8pEwjhLvcZOysVlRXE9fYFZ7heHoaihZmZIcnNPPi/SnNr1qVExgIWFYCf6QzpMdv7bMKag8AnYlalvbEIAyJA2tjZ0Gt9aQ9YlzmbGtyFX344481bSfLR/3fpNABO2j/6C6IQxxaGOPRiUeBEJ4VwPxmCUecRPWOHgQfyROReELWwkTIXZ17j0YeABDHWpsHASTjMdupvdwma20TlA3ruNV9WqDn1VE8hDTB4waAImqbZI0bBMdqDFVE0q50DSl2uzzO8X825CLjIa/E0U6JPid41hGOdadZph5Gbpnlou8xwOgRfzG1yyptPCKrAJcgIvsSz/CsYCqaoPCpil4TFjUq4PH0cWo6GlXN95TPX0LrAOh8WMCb7lZYXq5Q2TZ/sn5jF1GIiZZFWVUZujXK2og0I0 [...]
+		keyCompareUpdate := "HrMHUvE9Esvn7GnZ+vAynaIg/8wlB3r0zm0htmnwofY0a95jf9O5bkBT8pEwjhLvcZOysVlRXE9fYFZ7heHoaihZmZIcnNPPi/SnNr1qVExgIWFYCf6QzpMdv7bMKag8AnYlalvbEIAyJA2tjZ0Gt9aQ9YlzmbGtyFX344481bSfLR/3fpNABO2j/6C6IQxxaGOPRiUeBEJ4VwPxmCUecRPWOHgQfyROReELWwkTIXZ17j0YeABDHWpsHASTjMdupvdwma20TlA3ruNV9WqDn1VE8hDTB4waAImqbZI0bBMdqDFVE0q50DSl2uzzO8X825CLjIa/E0U6JPid41hGOdadZph5Gbpnlou8xwOgRfzG1yyptPCKrAJcgIvsSz/CsYCqaoPCpil4TFjUq4PH0cWo6GlXN95TPX0LrAOh8WMCb7lZYXq5Q2TZ/sn5jF1GIiZZFWVUZujXK2og0I042 [...]
 		// key update compare
 		err = s.NewSecret(secretName, certUpdate, keyUpdate)
 		assert.Nil(ginkgo.GinkgoT(), err, "create secret error")
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tlsUpdate, err := s.ListApisixTls()
+		tlsUpdate, err := s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tlsUpdate error")
 		assert.Len(ginkgo.GinkgoT(), tlsUpdate, 1, "tls number not expect")
 		assert.Equal(ginkgo.GinkgoT(), certUpdate, tlsUpdate[0].Cert, "tls cert not expect")
-		assert.Equal(ginkgo.GinkgoT(), key_compare_update, tlsUpdate[0].Key, "tls key not expect")
+		assert.Equal(ginkgo.GinkgoT(), keyCompareUpdate, tlsUpdate[0].Key, "tls key not expect")
 		// check DP
 		s.NewAPISIXHttpsClient(host).GET("/ip").WithHeader("Host", host).Expect().Status(http.StatusOK).Body().Raw()
 
@@ -264,7 +265,7 @@ UnBVSIGJ/c0AhVSDuOAJiF36pvsDysTZXMTFE/9i5bkGOiwtzRNe4Hym/SEZUCpn
 		assert.Nil(ginkgo.GinkgoT(), err, "delete tls error")
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err = s.ListApisixTls()
+		tls, err = s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 0, "tls number not expect")
 	})
diff --git a/test/e2e/ingress/ssl.go b/test/e2e/ingress/ssl.go
index 5511a86..ba8fe7f 100644
--- a/test/e2e/ingress/ssl.go
+++ b/test/e2e/ingress/ssl.go
@@ -16,6 +16,10 @@
 package ingress
 
 import (
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"net/http"
 	"time"
 
 	"github.com/onsi/ginkgo"
@@ -85,7 +89,7 @@ wrw7im4TNSAdwVX4Y1F4svJ2as5SJn5QYGAzXDixNuwzXYrpP9rzA2s=
 		assert.Nil(ginkgo.GinkgoT(), err, "create tls error")
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err := s.ListApisixTls()
+		tls, err := s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 1, "tls number not expect")
 	})
@@ -153,7 +157,7 @@ RU+QPRECgYB6XW24EI5+w3STbpnc6VoTS+sy9I9abTJPYo9LpCJwfMYc9Tg9Cx2K
 
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err := s.ListApisixTls()
+		tls, err := s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 1, "tls number not expect")
 		assert.Equal(ginkgo.GinkgoT(), tls[0].Snis[0], host, "tls host is error")
@@ -221,7 +225,7 @@ RU+QPRECgYB6XW24EI5+w3STbpnc6VoTS+sy9I9abTJPYo9LpCJwfMYc9Tg9Cx2K
 
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err := s.ListApisixTls()
+		tls, err := s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 1, "tls number not expect")
 		assert.Equal(ginkgo.GinkgoT(), tls[0].Snis[0], host, "tls host is error")
@@ -231,8 +235,290 @@ RU+QPRECgYB6XW24EI5+w3STbpnc6VoTS+sy9I9abTJPYo9LpCJwfMYc9Tg9Cx2K
 		assert.Nil(ginkgo.GinkgoT(), err, "delete tls error")
 		// check ssl in APISIX
 		time.Sleep(10 * time.Second)
-		tls, err = s.ListApisixTls()
+		tls, err = s.ListApisixSsl()
 		assert.Nil(ginkgo.GinkgoT(), err, "list tls error")
 		assert.Len(ginkgo.GinkgoT(), tls, 0, "tls number not expect")
 	})
 })
+
+var _ = ginkgo.Describe("ApisixTls mTLS Test", func() {
+	// RootCA -> Server
+	// RootCA -> UserCert
+	// These certs come from mTLS practice
+
+	rootCA := `-----BEGIN CERTIFICATE-----
+MIIF9zCCA9+gAwIBAgIUFKuzAJZgm/fsFS6JDrd+lcpVZr8wDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjI4WhcNMjIwNTI3MTMzNjI4
+WjCBnDELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEYMBYGA1UECgwPQVBJU0lYLVRlc3QtQ0FfMRgwFgYDVQQLDA9BUElT
+SVhfQ0FfUk9PVF8xFTATBgNVBAMMDEFQSVNJWC5ST09UXzEcMBoGCSqGSIb3DQEJ
+ARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+ALJR0lQW/IBqQTE/Oa0Pi4LlmlYUSGnqtFNqiZyOF0PjVzNeqoD9JDPiM1QRyC8p
+NCd5L/QhtUIMMx0RlDI9DkJ3ALIWdrPIZlwpveDJf4KtW7cz+ea46A6QQwB6xcyV
+xWnqEBkiea7qrEE8NakZOMjgkqkN2/9klg6XyA5FWfvszxtuIHtjcy2Kq8bMC0jd
+k7CqEZe4ct6s2wlcI8t8s9prvMDm8gcX66x4Ah+C2/W+C3lTpMDgGqRqSPyCW7na
+Wgn0tWmTSf1iybwYMydhC+zpM1QJLvfDyqjp1wJhziR5ttVe2Xc+tDC24s+u16yZ
+R93IO0M4lLNjvEKJcMltXyRzrcjvLXOhw3KirSHNL1KfrBEl74lb+DV5eU4pIFCj
+cu18gms5FBYs9tpLujwpHDc2MU+zCvRmSPvUA4yCyoXqom3uiSo3g3ymW9IM8dC8
++Bd1GdM6JbpBukvQybc5TQXo1M75I9iEoQa5tQxAfQ/dfwMjOK7skogowBouOuLv
+BEFKy3Vd57IWWZXC4p/74M6N4fGYTgHY5FQE3R4Y2phk/eaEm1jS1UPuC98QuTfL
+rGuFOIBmK5euOm8uT5m9hnrouG2ZcxEdzHYfjsGDGrLzA0FLu+wtMNBKM4NhsNCa
+d+fycLg7jgxWhaLvD5DfkV7WFQlz5LUceYIwYOyhD/chAgMBAAGjLzAtMAwGA1Ud
+EwQFMAMBAf8wHQYDVR0RBBYwFIISbXRscy5odHRwYmluLmxvY2FsMA0GCSqGSIb3
+DQEBCwUAA4ICAQCNtBmoAc5tv3H38sj9qhTmabvp9RIzZYrQSEcN+A2i3a8FVYAM
+YaugZDXDcTycoWn6rcgblUDneow3NiqZ57yYZmN+e4mE3+Q1sGepV7LoRkHDUT8w
+jAJndcZ/xxJmgH6B7dImTAPsvLGR7E7gffMH+aKCdnkG9x5Vm+cuBwSEBndiHGfr
+yw5cXO6cMUq8M6zJrk2V+1BAucXW2rgLTWy6UTTGD56cgUtbStRO6muOKoElDLbW
+mSj2rNv/evakQkV8dgKVRFgh2NQKYKpXmveMaE6xtFFf/dd9OhDFjUh/ksxn94FT
+xj/wkhXCEPl+t7tENhr2tNyLbCOVcFzqoi7IyoWKxxZQfvArfj4SmahK8E/BXB/T
+4PEmn8kZAxaW7RmGcaekm8MTqGlhCJ3tVJAI2vcYRdd9ZHbXE1jr/4xj0I/Lzglo
+O8v5fd4zHyV1SuZ5AH3XbUd7ndl9yDoN2WSqK9Nd9bws3yrf+GwjJAT1InnDvLg1
+stWM8I+9FZiDFL255/+iAN0jYcGu9i4TNvC+o6qQ1p85i1OHPJZu6wtUWMgDJN46
+uwW3ZLh9sZV6OnhbQJBQaUmcgaPJUQqbXNQmpmpc0NUjET/ltFRZ2hlyvvpf7wwF
+2DLY1HRAknQ69DuT6xpYz1aKZqrlkbCWlMMvdosOg6f7+4NxdYJ/rBeS6Q==
+-----END CERTIFICATE-----
+`
+
+	serverCertSecret := `server-secret`
+	serverCert := `-----BEGIN CERTIFICATE-----
+MIIF/TCCA+WgAwIBAgIUBbUP7Gk0WAb/JhYYcBBgZEgmhbEwDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjI5WhcNMjIwNTI3MTMzNjI5
+WjCBpTELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEcMBoGA1UECgwTQVBJU0lYLVRlc3QtU2VydmVyXzEXMBUGA1UECwwO
+QVBJU0lYX1NFUlZFUl8xGzAZBgNVBAMMEm10bHMuaHR0cGJpbi5sb2NhbDEcMBoG
+CSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAMZRPG8gUrOGM4awnV6D8Ds0Xb6jVbiGkx+1YsvPx5oIE4AswJ0l
+y6zqhBFnpQozFG63KfsCA6U36/Dty3rIbJzsbO7YaOMJItoiQgqdqF2nrmPpmpCQ
+uLGKaVvriRCD55NEmFQPshlRfcU5/EEreNKbRve3zEKHRpCDBZ2Myvrpt3CCVy6D
+MbLllbjUvaedrnQxlmI5d7x3UCe4Eunq8vn7c0p4frA1n8TxbX0M4Yr9g3YEEqCv
+Q3/9jU4hI5CvujCp+u79EavJZfsaEv3RYgHkoEh7q+OEkUajWXKj4WynizraWsHv
++LvK9pfI300p1HSKK4FqonvW79anRNbK+8BqV4Wt5aBeFU/rW2jHtJxcl1OLRrrh
+wftCP5W7vSjvJes5wPDZjDEyv8WP1Aa6yWeGHHtIwrAHPr7556F/JAQS6IPBQQ5U
+X45DD2aNXME9xZKdBtyMovItjZm31UUsvoF+YtpAOmbEkX4lMznUO3XZJjM5HWSq
+WYyzmFsw+pJEwhXRo4GfSfCHfiZQ6imTLJ7OsZzo9bvmxyfI0kVLe3h3iCe+qYeT
+f5AJU6v5vv3thCtfgbxYP2P8b+0MIrfr05e6dCDXbIri1z+nprzWYmyCrZ6H4hVk
+DzMktFUlkqenvnsJ2iOV2AZw0Hlk2bwe4zSumzqoIp8Yk/kxbfxhQqr5AgMBAAGj
+LDAqMAkGA1UdEwQCMAAwHQYDVR0RBBYwFIISbXRscy5odHRwYmluLmxvY2FsMA0G
+CSqGSIb3DQEBCwUAA4ICAQCDDfETCEpWB/KRQZo2JF8n4NEDTeraQ85M3H5luJHp
+NdJO4oYq3n8B149ep4FcEYdO20pV+TMeMNWXMfhoRIpGx95JrLuLg6qnw6eNdErn
+YupHMC2OEoEWVcmI052LDJcXuKsTXQvU4OeEL2dX4OtNJ+mRODLyh40cg7dA3wry
+kGLiprRlLQtiX8pSDG30qPZexL1LcFzBQajriG05QUrJW6Rvbq1JTIlyp7E1T86f
+Xljq0Hdzqxy+FklYcAW5ZAxgkQlMmVdTlvDXlD/hQLEQIHGHiW6OMLp8WrnJP6b0
+D2HqWmOwuEzqSgXSK0N89rpiWP1FKCpyiKVcsawDNfOpePVuthommVEc2PxacyHf
+UCC9V0MS0ZzQ63Tnz2Tja8C6/kMyVX226KQKhcoDxDoS0mQrI96/VXcglwP5hMjF
+joth1T1qRVu6+NQmvFPaNjbzWJ+j1R99bnYGihPeLdqDSUxNosV3ULG8T4aN6+f8
+hApiqg2dkLJQr8zWf6vWXMlREdPEovb2F7P0Lfn0VeOSRXDUIdqcoRHONi8bWMRs
+fjPtGW00Tv8Jg21c9vc8Zh/t1w3wkXQhqYiBMt5cYe6WueIlXdjF7ikSRWAHTwlw
+Bfzv/vMftLnbySPovCzQ1PF55D01EWRk0o6PRwUDLfzTQoV+bDKx82LxKtZBtQEX
+uw==
+-----END CERTIFICATE-----
+`
+	serverKey := `-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
+`
+	clientCASecret := `client-ca-secret`
+	clientCert := `-----BEGIN CERTIFICATE-----
+MIIGDDCCA/SgAwIBAgIUBbUP7Gk0WAb/JhYYcBBgZEgmhbIwDQYJKoZIhvcNAQEL
+BQAwgZwxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwI
+SGFuZ3pob3UxGDAWBgNVBAoMD0FQSVNJWC1UZXN0LUNBXzEYMBYGA1UECwwPQVBJ
+U0lYX0NBX1JPT1RfMRUwEwYDVQQDDAxBUElTSVguUk9PVF8xHDAaBgkqhkiG9w0B
+CQEWDXRlc3RAdGVzdC5jb20wHhcNMjEwNTI3MTMzNjMxWhcNMjIwNTI3MTMzNjMx
+WjCBtDELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREwDwYDVQQHDAhI
+YW5nemhvdTEfMB0GA1UECgwWQVBJU0lYLVRlc3QtUm9vdC1Vc2VyXzEaMBgGA1UE
+CwwRQVBJU0lYX1JPT1RfVVNFUl8xJDAiBgNVBAMMG0JpZ01pbmdfIDY1NDMxMTEx
+MTExMTExMTExMTEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL41i+W8fgqtKButnkOa0qGApPJ7HSUI
+gxBt6Tb7mR8eka8vno8hIE42hwMXYoIiO1FgM9M9fPN8vzTnWr2iTAXbw/qXdn14
+RT01JrGzj1OU3M9U0RMmDWVs8VOY3ncdcdPoqctMNr2K+6wlN6EpG1DQDUEvB0Yy
+qZpFWpuZP0akzjFqeJNSo3b8hP4J1nsWBxKt6TRInC60jT1mhKCjTB1IsXVC+5SY
+f0FhMN2ZQPgCyIIq+j5S7tHta4Qs+UDNY0whIMBkY3ZAGPzUBFExwrX7bGNbeJYz
+DahNAKiEj5dlieRvzTn5UisSv5EzDUONLSluEnCU1Ghd/dfZtZwlMBU6OIflwm6W
+X7HePD0G8XEDe5qbU/tg/62j+A1hMS2gSlibNisyvNwMNz6Imc3CnIFUL2mTsgl1
+bCBLfOXDfrjte5Txnt26ayN0+WhmVSts0+ln6vnGtgVGqH0bmp95yMRYPej+6ARE
+MtfvrNA7iZORypspv2NVyV11b6Jx3o/pJOAq9dmAsKvJ6hK0IVf/55t3ZtzRa/fY
+w0f78KbWuEc7qmx2tBxnEfppb5iB/RFmI2bDaAEA0lDQgAqmiAu1utetfqsCwA1m
+3VXGp8Vp+lexA/M3Bkb5BHa6C11QDHvGjUVwduOymLAHKumvdgf7TJFnlhpg6jYN
+unO58bRSTI6LAgMBAAGjLDAqMAkGA1UdEwQCMAAwHQYDVR0RBBYwFIISbXRscy5o
+dHRwYmluLmxvY2FsMA0GCSqGSIb3DQEBCwUAA4ICAQCoTBvw11aKah2cuB4XplUB
+nmkrWhfLNFJLVrU9jIISP9Wkl3s3PcM+aEWygb/1roqbMqNOgrzDVgGRRFCiS6qi
+himUuTJhIBI6TF1PE+XW3gTFWBXkAZ7MzpbS8oP1PehlY3XXKNZgxZi3XaDI8Hfw
+5MWBGNbk8tegn8bvYQUz2VxmCo6zufCkj4ADjw2zhiyKBKuHTzg48w66Wn4jLhlK
+p91HHrK0lEOIJ4pFmBUpBsSJMlBMbfrzBF87xQhpDO3ScFfCWUatShmXsPMJU0F0
+DEuTnaHUefUf/F9wUGNcA4yQ4pH8SxVpRHmrWE8U4uSXpz1bx4ChZurJ9mPzrj9h
+U9c/d9F5WndZNPcR1R8Tbzhk/R8GImVR3Lt59cW+SN/+4JVFy+Hye2yslGFn2CAJ
+ofNxjLb9OE6+EE3SWW0B5CZSWBS49gtdTW0ApOjIRJU2zipxcjnNf00GFoIoCxjk
+Z4eBQz9WVUM9KSrJIQSLZQd35tZAOp0BuwWho0+w8lXUchSqT7oRA7+szZldWF0j
+HKPIMJ0iVWmXuZjsS8q8NBIt4DuBcqpevlol5KRXv6tJy4IBVAVEIBdeXotvdxKE
+bncvZ6xo9A/waUU7tEyzv34usxefrWxtSlOA1G0Jj4nb5gKPHjn0XIr9WI2RpovT
+/XpB6QES1zoBQya3QjnDbQ==
+-----END CERTIFICATE-----
+`
+
+	clientKey := `-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
+`
+
+	s := scaffold.NewDefaultV2Scaffold()
+	ginkgo.It("create a SSL with client CA", func() {
+		// create secrets
+		err := s.NewSecret(serverCertSecret, serverCert, serverKey)
+		assert.Nil(ginkgo.GinkgoT(), err, "create server cert secret error")
+		err = s.NewClientCASecret(clientCASecret, rootCA, "")
+		assert.Nil(ginkgo.GinkgoT(), err, "create client CA cert secret error")
+
+		// create ApisixTls resource
+		tlsName := "tls-with-client-ca"
+		host := "mtls.httpbin.local"
+		err = s.NewApisixTlsWithClientCA(tlsName, host, serverCertSecret, clientCASecret)
+		assert.Nil(ginkgo.GinkgoT(), err, "create ApisixTls with client CA error")
+		// check ssl in APISIX
+		time.Sleep(10 * time.Second)
+		apisixSsls, err := s.ListApisixSsl()
+		assert.Nil(ginkgo.GinkgoT(), err, "list ssl error")
+		assert.Len(ginkgo.GinkgoT(), apisixSsls, 1, "ssl number not expect")
+
+		// create route
+		backendSvc, backendSvcPort := s.DefaultHTTPBackend()
+		apisixRoute := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2alpha1
+kind: ApisixRoute
+metadata:
+  name: httpbin-route
+spec:
+  http:
+  - name: rule1
+    match:
+      hosts:
+      - mtls.httpbin.local
+      paths:
+      - /*
+    backend:
+      serviceName: %s
+      servicePort: %d
+`, backendSvc, backendSvcPort[0])
+		assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute))
+		time.Sleep(10 * time.Second)
+
+		apisixRoutes, err := s.ListApisixRoutes()
+		assert.Nil(ginkgo.GinkgoT(), err, "list routes error")
+		assert.Len(ginkgo.GinkgoT(), apisixRoutes, 1, "route number not expect")
+
+		// Without Client Cert
+		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))
+		assert.True(ginkgo.GinkgoT(), ok, "Append cert to CA pool")
+
+		cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
+		assert.Nil(ginkgo.GinkgoT(), err, "generate cert")
+
+		s.NewAPISIXHttpsClientWithCertificates(host, true, caCertPool, []tls.Certificate{cert}).
+			GET("/ip").WithHeader("Host", host).Expect().Status(http.StatusOK)
+	})
+})
diff --git a/test/e2e/scaffold/k8s.go b/test/e2e/scaffold/k8s.go
index da5ede6..4f4a912 100644
--- a/test/e2e/scaffold/k8s.go
+++ b/test/e2e/scaffold/k8s.go
@@ -305,8 +305,8 @@ func (s *Scaffold) ListApisixStreamRoutes() ([]*v1.StreamRoute, error) {
 	return cli.Cluster("").StreamRoute().List(context.TODO())
 }
 
-// ListApisixTls list all ssl from APISIX
-func (s *Scaffold) ListApisixTls() ([]*v1.Ssl, error) {
+// ListApisixSsl list all ssl from APISIX
+func (s *Scaffold) ListApisixSsl() ([]*v1.Ssl, error) {
 	u := url.URL{
 		Scheme: "http",
 		Host:   s.apisixAdminTunnel.Endpoint(),
diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go
index 5e68ff5..2e0998c 100644
--- a/test/e2e/scaffold/scaffold.go
+++ b/test/e2e/scaffold/scaffold.go
@@ -17,6 +17,7 @@ package scaffold
 import (
 	"context"
 	"crypto/tls"
+	"crypto/x509"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -126,6 +127,19 @@ func NewDefaultScaffold() *Scaffold {
 	return NewScaffold(opts)
 }
 
+// NewDefaultV2Scaffold creates a scaffold with some default options.
+func NewDefaultV2Scaffold() *Scaffold {
+	opts := &Options{
+		Name:                  "default",
+		Kubeconfig:            GetKubeconfig(),
+		APISIXConfigPath:      "testdata/apisix-gw-config.yaml",
+		IngressAPISIXReplicas: 1,
+		HTTPBinServicePort:    80,
+		APISIXRouteVersion:    kube.ApisixRouteV2alpha1,
+	}
+	return NewScaffold(opts)
+}
+
 // KillPod kill the pod which name is podName.
 func (s *Scaffold) KillPod(podName string) error {
 	cli, err := k8s.GetKubernetesClientE(s.t)
@@ -191,7 +205,7 @@ func (s *Scaffold) NewAPISIXClientWithTCPProxy() *httpexpect.Expect {
 	})
 }
 
-// NewAPISIXHttpsClient creates the default HTTPs client.
+// NewAPISIXHttpsClient creates the default HTTPS client.
 func (s *Scaffold) NewAPISIXHttpsClient(host string) *httpexpect.Expect {
 	u := url.URL{
 		Scheme: "https",
@@ -214,6 +228,30 @@ func (s *Scaffold) NewAPISIXHttpsClient(host string) *httpexpect.Expect {
 	})
 }
 
+// NewAPISIXHttpsClientWithCertificates creates the default HTTPS client with giving trusted CA and client certs.
+func (s *Scaffold) NewAPISIXHttpsClientWithCertificates(host string, insecure bool, ca *x509.CertPool, certs []tls.Certificate) *httpexpect.Expect {
+	u := url.URL{
+		Scheme: "https",
+		Host:   s.apisixHttpsTunnel.Endpoint(),
+	}
+	return httpexpect.WithConfig(httpexpect.Config{
+		BaseURL: u.String(),
+		Client: &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: insecure,
+					ServerName:         host,
+					RootCAs:            ca,
+					Certificates:       certs,
+				},
+			},
+		},
+		Reporter: httpexpect.NewAssertReporter(
+			httpexpect.NewAssertReporter(ginkgo.GinkgoT()),
+		),
+	})
+}
+
 // APISIXGatewayServiceEndpoint returns the apisix http gateway endpoint.
 func (s *Scaffold) APISIXGatewayServiceEndpoint() string {
 	return s.apisixHttpTunnel.Endpoint()
diff --git a/test/e2e/scaffold/ssl.go b/test/e2e/scaffold/ssl.go
index cdde5ab..4b84134 100644
--- a/test/e2e/scaffold/ssl.go
+++ b/test/e2e/scaffold/ssl.go
@@ -32,6 +32,14 @@ data:
   cert: %s
   key: %s
 `
+	_clientCASecretTemplate = `
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+data:
+  cert: %s
+`
 	_api6tlsTemplate = `
 apiVersion: apisix.apache.org/v1
 kind: ApisixTls
@@ -44,6 +52,23 @@ spec:
     name: %s
     namespace: %s
 `
+	_api6tlsWithClientCATemplate = `
+apiVersion: apisix.apache.org/v1
+kind: ApisixTls
+metadata:
+  name: %s
+spec:
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+  client:
+    caSecret:
+      name: %s
+      namespace: %s
+    depth: 10
+`
 )
 
 // NewSecret new a k8s secret
@@ -57,6 +82,16 @@ func (s *Scaffold) NewSecret(name, cert, key string) error {
 	return nil
 }
 
+// NewSecret new a k8s secret
+func (s *Scaffold) NewClientCASecret(name, cert, key string) error {
+	certBase64 := base64.StdEncoding.EncodeToString([]byte(cert))
+	secret := fmt.Sprintf(_clientCASecretTemplate, name, certBase64)
+	if err := k8s.KubectlApplyFromStringE(s.t, s.kubectlOptions, secret); err != nil {
+		return err
+	}
+	return nil
+}
+
 // NewApisixTls new a ApisixTls CRD
 func (s *Scaffold) NewApisixTls(name, host, secretName string) error {
 	tls := fmt.Sprintf(_api6tlsTemplate, name, host, secretName, s.kubectlOptions.Namespace)
@@ -66,6 +101,15 @@ func (s *Scaffold) NewApisixTls(name, host, secretName string) error {
 	return nil
 }
 
+// NewApisixTlsWithClientCA new a ApisixTls CRD
+func (s *Scaffold) NewApisixTlsWithClientCA(name, host, secretName, clientCASecret string) error {
+	tls := fmt.Sprintf(_api6tlsWithClientCATemplate, name, host, secretName, s.kubectlOptions.Namespace, clientCASecret, s.kubectlOptions.Namespace)
+	if err := k8s.KubectlApplyFromStringE(s.t, s.kubectlOptions, tls); err != nil {
+		return err
+	}
+	return nil
+}
+
 // DeleteApisixTls remove ApisixTls CRD
 func (s *Scaffold) DeleteApisixTls(name string, host, secretName string) error {
 	tls := fmt.Sprintf(_api6tlsTemplate, name, host, secretName, s.kubectlOptions.Namespace)