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 <[email protected]>
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-----
+MIIJKQIBAAKCAgEAvjWL5bx+Cq0oG62eQ5rSoYCk8nsdJQiDEG3pNvuZHx6Rry+e
+jyEgTjaHAxdigiI7UWAz0z1883y/NOdavaJMBdvD+pd2fXhFPTUmsbOPU5Tcz1TR
+EyYNZWzxU5jedx1x0+ipy0w2vYr7rCU3oSkbUNANQS8HRjKpmkVam5k/RqTOMWp4
+k1KjdvyE/gnWexYHEq3pNEicLrSNPWaEoKNMHUixdUL7lJh/QWEw3ZlA+ALIgir6
+PlLu0e1rhCz5QM1jTCEgwGRjdkAY/NQEUTHCtftsY1t4ljMNqE0AqISPl2WJ5G/N
+OflSKxK/kTMNQ40tKW4ScJTUaF3919m1nCUwFTo4h+XCbpZfsd48PQbxcQN7mptT
++2D/raP4DWExLaBKWJs2KzK83Aw3PoiZzcKcgVQvaZOyCXVsIEt85cN+uO17lPGe
+3bprI3T5aGZVK2zT6Wfq+ca2BUaofRuan3nIxFg96P7oBEQy1++s0DuJk5HKmym/
+Y1XJXXVvonHej+kk4Cr12YCwq8nqErQhV//nm3dm3NFr99jDR/vwpta4RzuqbHa0
+HGcR+mlvmIH9EWYjZsNoAQDSUNCACqaIC7W6161+qwLADWbdVcanxWn6V7ED8zcG
+RvkEdroLXVAMe8aNRXB247KYsAcq6a92B/tMkWeWGmDqNg26c7nxtFJMjosCAwEA
+AQKCAgAHKzF4mSAO+vO2B1cdqSojGBwfX3B7wtRdvCa8AcOFnrtS5PKO5mq3R+rS
+vQDjcrLVoFCTt4+MBbmXHtkWqJVA60V5nlfC5tOFOQmaTPAr8EJaNhIjLJ34oqB9
+zBcmWh++ItizZs3xWtmdZVGxa0EyTIUTXdhiVup5e/+sOZxe5zs2NZMRyl2K0H2a
+rXg971iY5aESbWIliHyCQejhvQXTXLgDeWDN+ulg527WCz6dmk1ASqpfyvRhSRdy
+RdenD5aceesoFSCChmvqq3r2LG/wN+ef3wSudIIhQ7WwpD5dMGCAEY6kjrcAFJbP
+vCLV1u5Kz3E2eQWAYXp9tiDYH7auJWoOIvIMIAuWcPVtu/XmQt8kNCxLvnS4gZpD
+i3DFTrziA/5+Qn1y2rI3V4jn/sWai9r92dfEhZiWtZ8sh0K3d5qMj9mWgQ4+KjX3
+HICZWDUOdMUeyfYgmVSEGxgcAZqj7JSGcMZCzxH2W9zMspQ+KWKr+YjIiw7YTLfj
+r4lzR89G+Wdqr/BCvAEEfm3S0j3Xcwytnm9ljdiwEXpIBwhyfzJjkfTAGdoPbqFS
+CScpO02m18ma/wwkDHuqJ7Zijvbybv7syk9byyxXCcckl+cn3agzdxh9AlJg6ASO
+IqAWtnM7x7/WwqZfxbUXo/GPjpR+1XJksHREJ+G1zokMyZNKIQKCAQEA8+jqv7V7
+UuBloJxlUZ7+5t7H8LX14VheW+kNrWxUoyp6p72HCulm/Vq8y8kkI6nXCvmIUSoS
+wMZa38DWXGcfq4nU7dBV7fRqvBEy+3sbBjJBKaxPmi3atlYmm12GC9aaL4Z7hwHm
+Sa+YeKxNH7dIIbom9SHT6c0/v1/zEit4c36z4dKHsUlobFx6NqJvjGdAAVDYR5hc
+56pEoMDkQsmFKzHo7sZxxrAvaaTrjJo7lgCC7fjQuXs2DSlaYcoZxO2GZ7mPj48z
+Jz57xDksll/LbqgYAhK84m6ioaTM8uAU72FKceC3VO2VoUNMjXDoOHIRNNu2rQjU
+iD4X1yiC65K1gwKCAQEAx6Myf8l4ijZIPuGwLgBWQV1ID+3V86+1cNB8hRi4s+6p
+apvakfzGgcuBWUqYqBwxflLuO4XaX4tp/DneOwSo+m2w126FCYlAPcPL3A+PYnG0
+fbf5PuKxW1kHkJeR5KeXENT2w+aTKlDvrWYGYtLW+xFZca/LIxVDsKb2iGre8mDb
+lIzRxfopAzOU0P/rI6CE0482LAcDfjxCxN3uzRhDp+f0d4T6/doYCd7rt7KZ29ww
+lpRrSbW4psM9s//VnBKdJUrUbf5DftRPUm0bhD0V0FgCP/E/louLS90d0aVRpC9F
+7kAYn3fb/wAkLUvcYM0WfM9PtxkT+wgaW4uy5CB8WQKCAQEA0cVD/9TpV4G+Zb+c
+M/J2b8CyXIdiDIifvpRVOw2sTRg/nPwXpH7QIJ1lOi6ncjSjycCKSKPStRDjHwUO
+VzIpvrIv+sfu31QSZ+Sy4C4kM9QMzvZvD77YF3FIit6IZq4OtUkH/DjaAg2PKFmn
+ittqofcjgjextabcaI7w0nOoiEw0EMesBAGKWYe/ZDWXkj1Kgtcw64JShLufglHi
+/r2qVlf6aUEqoSLt5AH+w1HyZTPTZy9S8/LPrcoe/XN/biqKKbMhkOorqFjIwR4b
+BskkgOr4mu/amzNjk3nU+h1WY/pcuEv34Ibk5Win8g1k6wbPXZKJLZAmmXYtstIY
+ptnqWQKCAQAD3+8C++4TAKq2TbsVqXwDGMRlSsB0Uly7K9C+5JPxKhivsQa0/qr7
+qe+AxCniWWm8ge+NyDNM12/fLWBa1ORSt/5OsB506O0ORdaXFtY5mutd5Uw5JD09
+AKVc8RQr0/Tipr+DXd5NW/TK8Mf+8wipJtUNl9PhgnAl5ZezXh+lpKueXn1T0l8p
+aL7ir5ToxBzP3l+2ywwOTy0clRIleOsXPzFHgJU+iBUfW+xHTHggBE4NHiRW8ef7
+lJ6F99k1hkb2ilVFLUIyG/zOJL/7+ROLT6n7g7swONUjS89gWk0TWreIwEW6EqF6
+eY46Mta8Kj7dfUiWzS3OGYIpdLSsKNVBAoIBAQC+oHivmfh0EF5DSn1jhXSB024w
+uEF7wZbH9PtzBshxU7onPaUzA+REBlooW7Aevg1I9aNyvCErJznzzF4E3Sm4JlY4
+pXAxNqpTcGurUt1MPjkgGmhVs5hNrkOJA5qcdvMO3DjOYfKl0X3LWXE3yPL82ztq
+kUp9iFjcpERPOQ8fU5RpmQazGtxck14EG47BlpHmGVf2eyidbXTMyxA7KpQ1tKKS
+RAmKucXUNJSR8wYGSg5ymvsnChTaYHLL1gmIdQli2y8XxqUaYC1tXrEt4g5z4a/O
++LD4uA7Fy2PdgiYSDlxA+u6lYI670sh3MR4tV7qssTK+U4735IlN3LxL1Fqn
+-----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)

Reply via email to