mTLS (Mutual TLS)認証はRFC8705に記載されている、TLSハンドシェイクの際にサーバーからだけではなくクライアントからも証明書を送ることで相互に認証を行う手法。クライアントは事前に自身の秘密鍵で生成したCSRをサーバーに送り、サーバーがルート認証局となって発行した証明書を取得しておく。
KubernetesではIstioのSidecarを通すことで透過的にmTLS認証を行うことができる。証明書はEnvoy SDS(secret discovery service) APIのリクエストによってCSRが定期的にistiodに送られて発行、更新されるらしい。
Istio OperatorでIstioをインストールする。 以前はHelmで入れていたが今はこれがベストプラクティスのようだ。
IstioをHelmでインストールしてRoutingとTelemetryを行いJaeger/Kialiで確認する - sambaiz-net
$ curl -L https://istio.io/downloadIstio | sh -
$ istio-1.8.0/bin/istioctl version
no running Istio pods in "istio-system"
1.8.0
$ istioctl operator init
Installing operator controller in namespace: istio-operator using image: docker.io/istio/operator:1.8.0
Operator controller will watch namespaces: istio-system
✔ Istio operator installed
✔ Installation complete
$ kubectl get pods --namespace istio-operator
NAME READY STATUS RESTARTS AGE
istio-operator-76766bc79-lfm49 1/1 Running 0 2m32s
$ kubectl create ns istio-system
$ kubectl apply -f - <<EOF
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
namespace: istio-system
name: example-istiocontrolplane
spec:
profile: demo
EOF
リクエスト先のnginxを立てる。
$ kubectl create ns sambaiz
$ kubectl label namespace sambaiz istio-injection=enabled
$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
namespace: sambaiz
spec:
selector:
matchLabels:
run: my-nginx
replicas: 1
template:
metadata:
labels:
run: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx
namespace: sambaiz
labels:
run: my-nginx
spec:
ports:
- port: 80
protocol: TCP
selector:
run: my-nginx
EOF
レスポンスが返ってくることを確認する。
$ kubectl label namespace default istio-injection=disabled --overwrite
$ kubectl run -it --rm=true test-client --image=curlimages/curl --restart=Never -- -s -o /dev/null -w '%{http_code}\n' my-nginx.sambaiz.svc.cluster.local
200
PeerAuthenticationでmTLSをSTRICTにするとリクエストを受ける際にmTLSが強制される。
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: nginx-auth
namespace: sambaiz
spec:
selector:
matchLabels:
run: my-nginx
mtls:
mode: STRICT
EOF
Sidecarなしでは弾かれるようになった。Sidecarを入れるとトラフィックが自動でmTLSにアップグレードされるので通る。
$ kubectl label namespace default istio-injection=disabled --overwrite
$ kubectl run -it --rm=true test-client --image=curlimages/curl --restart=Never my-nginx.sambaiz.svc.cluster.local
curl: (56) Recv failure: Connection reset by peer
$ kubectl label namespace default istio-injection=enabled --overwrite
$ kubectl run -it --rm=true test-client --image=curlimages/curl --restart=Never sh
/ $ curl -s -o /dev/null -w '%{http_code}\n' my-nginx.sambaiz.svc.cluster.local
200
指定したhostにリクエストを送る際に適用されるDestinationRuleの TLSModeを ISTIO_MUTUALにするとIstio生成の証明書が、MUTUALにすると自分で生成した証明書が送られ、DISABLEだとTLS通信を行わなくなる。 hostにservice名のみを指定した場合そのRuleのnamespaceのものとして解釈されるが、ミスを防ぐために my-svc.my-namespace.svc.cluster.local のようなFQDNで指定することが推奨されている。
DISABLEにしてリクエストを送るとSidecarがあっても弾かれる。
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: sambaiz
spec:
host: "*.sambaiz.svc.cluster.local"
trafficPolicy:
tls:
mode: DISABLE
EOF
$ kubectl label namespace default istio-injection=enabled --overwrite
$ kubectl run -it --rm=true test-client --image=curlimages/curl --restart=Never sh
/ $ curl my-nginx.sambaiz.svc.cluster.local
upstream connect error or disconnect/reset before headers. reset reason: connection termination
$ kubectl delete destinationrule sambaiz
mTLS認証を行うと証明書の SAN(Subject Alternative Name) に書かれているSPIFFE IDが By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=;Subject="";URI=spiffe://cluster.local/ns/foo/sa/sleep のような形式で X-Forwarded-Client-Cert headerに乗って渡るので、これを基にAuthorizationPolicyによるアクセス制御ができる。
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: nginx-authn
namespace: sambaiz
spec:
selector:
matchLabels:
run: my-nginx
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/test-service-account"]
to:
- operation:
methods: ["GET"]
EOF
AuthorizationPolicyを作成するとprincipalsのServiceAccountを持たないクライアントからのリクエストは弾かれるようになった。
$ kubectl run -it --rm=true test-client --image=curlimages/curl --restart=Never sh
/ $ curl my-nginx.sambaiz.svc.cluster.local
RBAC: access denied/
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: test-service-account
automountServiceAccountToken: false
EOF
$ kubectl run --serviceaccount=test-service-account -it --rm=true test-client --image=curlimages/curl --restart=Never sh
/ $ curl -s -o /dev/null -w '%{http_code}\n' my-nginx.sambaiz.svc.cluster.local
200
参考
【図解】mutual-TLS (mTLS, 2way TLS),クライアント認証とトークンバインディング over http | SEの道標