IstioのSidecarでmTLS認証を行いServiceAccountによるアクセス制御を行う

kubernetesistioauth

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にリクエストを送る際に適用されるDestinationRuleTLSModeISTIO_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の道標

注目のSPIFFE、その概要とKubernetesへの導入方法 | Think IT(シンクイット)