Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する連載。今回は、Istioのセキュリティ機能に焦点を当て、通信の暗号化、認証/認可の機能を紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する本連載「Cloud Nativeチートシート」。前回、前々回とサービスメッシュ、特にIstioの機能について説明してきましたが、今回も引き続きIstioの機能について紹介していきます。今回はIstioのセキュリティ機能に焦点を当て、通信の暗号化、認証/認可の機能を紹介します。
マイクロサービスアーキテクチャでは、アジリティの向上、スケーラビリティの向上、サービスの再利用が可能になるなど、さまざまなメリットが得られます。しかし、複数のサービスが連携して1つのシステムを構成する特徴から、モノリシックアーキテクチャでは考慮する必要のなかった点が課題となります。
例として、以下のようなリスクが存在します。
モノリシックなアプリケーションでは、サービスは1つのネットワークにあるサーバで動作するので、公開されたエンドポイントに着目したり、サーバのIPアドレスを特定することが可能であったりと、セキュリティ対策が比較的容易でした。
一方、マイクロサービスでは、サービスが頻繁かつ動的に分散配置されるので、割り当てられるIPアドレスが短期間で変化します。また、サービス間で通信し、サービス単位にエンドポイントを用意するので、不正アクセスされるリスクが高まります。
モノリシックなサービスの場合、プロセス間はメモリを用いて通信することが一般的でした。そのため、公開されたエンドポイントとの通信を暗号化することで一定のセキュリティレベルを保証できます。
一方マイクロサービスの場合、ネットワーク経由でサービス同士が通信し合うことになります。そのため、仮にマイクロサービス間の通信が暗号化されていない場合、第三者から通信を傍受されるリスクが存在します。
上記で挙げたセキュティリスクは一部にすぎませんが、確実に対策する必要があります。下表の対策方法が一般的でしょう。
脅威(被害) | セキュリティ対策 | |
---|---|---|
不正アクセス(情報資産の改ざん/変更、情報資産の破壊/削除、情報資産の流出) | 各マイクロサービス間での認証 | |
外部アクセスユーザーの認証 | ||
サービス/ユーザーの認可(アクセス権の設定) | ||
盗聴(情報資産/個人情報の流出) | 外部からの通信の暗号化 | |
サービス間通信の暗号化 | ||
これらの最も単純な実現方法には、各サービスを構成するアプリケーションに、サービス間通信を規定するロジックを実装することが考えられます。しかし、全てのサービスで該当ロジックを維持することは困難なので、このアプリケーション層で全て吸収する方法はあまり現実的ではありません。
そこで本記事では、Istioの機能を活用し、マイクロサービスの課題を解決する方法を解説します。「Istioがどのような仕組みでセキュリティ機能を構成するのか」「どのリソースを使うことで、上記のセキュリティ課題への対策が可能になるのか」を確認します。
Istioのセキュリティ機能は主に4つです。
各セキュリティ機能は、これまで紹介したリソースと同様にコントロールプレーンの「Istiod」コンポーネントをからデータプレーンに対して関連する設定を伝達、管理するだけで稼働します。つまり、今回もアプリケーションのソースコードを変更する必要はありません。
以下、4つのセキュリティ機能の概要をします。
前述の不正アクセスリスクの対策としては認証機能の導入、盗聴へのリスクに対しては暗号化された通信があります。「Ingress Gateway」はTLSに対応しており、クラスタ外からクラスタ内への通信を暗号化し、終端することが可能です。証明書と鍵をIngress Gatewayにマウントすることで簡単に実装できます。
この機能は、外部にサービスを公開する場合など最も使用される機能といえるでしょう。
なおTLS終端には、「Gateway」リソースが使用されます。
Istioでは、X.509証明書を利用した相互TLS認証(mTLS)が提供されており、マイクロサービス間の認証と通信の暗号化も実現可能です。
「内部ネットワークなのに相互認証させる必要があるのか?」と思う方もいると思います。確かに内部通信まで認証するのは過剰なケースもあるでしょう。しかし、機密性の高い顧客データをマイクロサービス間で送信する場合はどうでしょうか。ネットワークにアクセスできる人は誰でもこの機密データを読み取って要求を偽造できる可能性があります。特に各サービスの開発チームが異なる場合は、内部脅威も考慮しなければなりません。このようなケースには相互TLS認証で脅威を最小限にします。
なお具体的な仕組みとして、IstioのIDと証明書の管理は、「SPIFFE(Secure Production Identity Framework For Everyone)」仕様を使います。SPIFFEの詳細については本記事では割愛しますが、Istioでは「X.509」証明書形式の「SVID」(SPIFFE Verifiable Identity Document)を用いることで、各サービス間の通信においてサービス相手の正当性を検証します。
仕組みは通常のPKI(Public Key Infrastructure)と同様です。まず、デフォルトではCA(Certification Authority:認証局)を管理するIstiodで証明書を発行し、「Envoy」に配布します。通信時にはこの証明書を互いに送信し、CAの公開鍵でSVIDを検証することでサービス相手の正当性を確認できます。また、これらの証明書の発行とローテーションはIstiodで自動化されているので、不要な運用コストを削減することもできます。
この相互TLS認証を設定するリソースとしては、「PeerAuthentication」が使用されます。
相互TLS認証によるマイクロサービス間の認証に加えて、JWT検証によるリクエストレベルの認証が実装されており、認証トークンを検証するエンドユーザー認証が可能です。「Auth0」「Keycloak」といった認証プロバイダーなどを使えるので、独自に作り込むことなく認証機能を実現できます。
なお、JWT検証は個別のマイクロサービスにも適用できますが、多くの場合はIngress Gatewayに適用され、設定するリソースとしては、「RequestAuthentication」が使用されます。
ここでの「アクセス制御」とは、リクエスト元のサービスのアクセス権限を確認した上でリクエストを「許可/拒否する」認可処理のことを指します。Istioは、相互認証やJWT検証などの認証機能と組み合わせて、各主体に対しての認可を設定する機能を持っています。具体的には「AuthorizationPolicy」を用いて、認証された主体が各サービスへのアクセスの可否などの操作を定義できます。
ここまでIstioの4つのセキュリティ機能の概要を確認しました。各機能をマイクロサービスに適用することで、セキュリティリスクを低減可能です。
また、それぞれの機能を使用するには、Istioのリソースを利用する必要がありました。これ以降、これらのリソースを用いてサンプルの「Bookinfo」アプリケーションにセキュリティ機能を適用しますが、リソースの関係が分からなくなった場合には、こちらの表を見て関係性を再度把握してみてください。
脅威(被害) | セキュリティ対策 | Istioの機能 | Istioのリソース |
---|---|---|---|
不正アクセス(情報資産の改ざん/変更、情報資産の破壊/削除、情報資産の流出) | 各マイクロサービス間での認証 | 相互TLS認証 | PeerAuthentication |
外部アクセスユーザーの認証 | JWT検証 | RequestAuthentication | |
サービス/ユーザーの認可(アクセス権の設定) | アクセス制御機能(認可機能) | AuthorizationPolicy | |
盗聴(情報資産/個人情報の流出) | 外部からの通信の暗号化 | TLS終端 | Gateway |
サービス間通信の暗号化 | 相互TLS認証 | PeerAuthentication | |
今回は、下記の環境で環境を構築しました。本稿でもインストール手順を記載しますが、詳細なインストール方法を知りたい方は、連載第10回を参考にしてください。
対象 | 説明 | 動作確認で利用した環境 |
---|---|---|
Kubernetes | Azure Kubernetes Service、Google Kubernetes Engineなどのクラウドのマネージドサービス環境や「minikube」などのローカル環境おけるKubernetesクラスタ | Azure Kubernetes Service(v1.21) |
Istio | サービスメッシュソフトウェア | v1.11.3 |
セキュリティ機能を適用する前に、まずはBookinfoアプリケーションを使用して通信の状態を見てみましょう。事前確認として平文でのサービス間の通信が可能かどうかを確認します。
なお、サービスメッシュ内との通信とサービスメッシュ外の通信の両方を比較するために「Sleep」アプリケーションもデプロイしています。
# Istioダウンロード:最新版を使用する場合(今回は1.11.3を使用) $ curl -L https://istio.io/downloadIstio | sh - # 事前にコマンドのパスを通しておく $ cd ./istio-1.11.3 $ export PATH=$PWD/bin:$PATH # Istioのインストール $ istioctl install --set profile=demo -y # 対象のnamespaceでサイドカープロキシの自動挿入が有効ではない場合は、有効にしてください $ kubectl create namespace sample-app $ kubectl label namespace sample-app istio-injection=enabled # アプリケーションのデプロイ $ kubectl apply -f ./samples/bookinfo/platform/kube/bookinfo.yaml -n sample-app $ kubectl apply -f ./samples/sleep/sleep.yaml -n sample-app $ kubectl apply -f ./samples/sleep/sleep.yaml -n default # 反映確認 $ kubectl get pod -n sample-app NAME READY STATUS RESTARTS AGE details-v1-79f774bdb9-l2mx2 2/2 Running 0 6h1m productpage-v1-6b746f74dc-ht2t2 2/2 Running 0 126m ratings-v1-b6994bb9-p7gpz 2/2 Running 0 6h1m reviews-v1-545db77b95-4s8zw 2/2 Running 0 5h56m reviews-v2-7bf8c9648f-nnxw2 2/2 Running 0 6h1m reviews-v3-84779c7bbc-kbdzs 2/2 Running 0 6h1m sleep-557747455f-mmkk7 2/2 Running 0 30s $ kubectl get pod NAME READY STATUS RESTARTS AGE sleep-557747455f-xtl8p 1/1 Running 0 28s
ここまでで事前準備は完了です。以降で前述の4つのセキュリティ機能単位にリソースの使用方法や具体的な挙動を確認します。
外部からのアクセスをTLS通信とし、Ingress Gatewayで終端させる設定を試します。今回は、自己証明書を使用したサーバ認証を設定します。ホスト「bookinfo.istio-sample.com」に対して自己証明書を作成する手順を紹介します。
# 自己証明書の作成 $ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=bookinfo/CN=Istio-sample.com' -keyout istio-sample.com.key -out istio-sample.com.crt $ openssl req -out bookinfo.istio-sample.com.csr -newkey rsa:2048 -nodes -keyout bookinfo.istio-sample.com.key -subj "/CN=bookinfo.istio-sample.com/O=bookinfo.istio-sample" $ openssl x509 -req -days 365 -CA istio-sample.com.crt -CAkey istio-sample.com.key -set_serial 0 -in bookinfo.istio-sample.com.csr -out bookinfo.istio-sample.com.crt
証明書が作成できたら、秘密鍵と証明書を「Secret」リソースとして登録します。
# シークレットリソースの作成 kubectl create -n istio-system secret tls bookinfo-credential --key=bookinfo.istio-sample.com.key --cert=bookinfo.istio-sample.com.crt
「Ingress Gatewayで終端させる」と先述しましたが、その設定はGatewayで行います。HTTPSで「bookinfo.istio-sample.com」に対する通信を受け入れます。「tls.mode」の「SIMPLE」が通常のTLSモードです。証明書はSecretの名前で指定します。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: bookinfo-gateway namespace: sample-app spec: selector: istio: ingressgateway servers: - port: number: 443 name: https protocol: HTTPS tls: mode: SIMPLE credentialName: bookinfo-credential #先ほど作成したSecretを指定 hosts: - bookinfo.istio-sample.com
また、Gatewayと各アプリケーションをVirtual Serviceでひも付ける必要があります。「hosts」にbookinfo.istio-sample.comを追加します。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: bookinfo namespace: sample-app spec: hosts: - "bookinfo.istio-sample.com" gateways: - bookinfo-gateway http: - match: - uri: exact: /productpage - uri: prefix: /static - uri: exact: /login - uri: exact: /logout - uri: prefix: /api/v1/products route: - destination: host: productpage port: number: 9080
準備ができたら、リソースを反映します。
# 設定の反映 $ kubectl apply -f ./tls_termination.yaml $ kubectl apply -f ./virtualservice_tls.yaml # リソースの確認 $ kubectl get gateway -n sample-app NAME AGE bookinfo-gateway 12s $ kubectl get virtualservice -n sample-app NAME GATEWAYS HOSTS AGE bookinfo ["bookinfo-gateway"] ["*"] 24s
設定が完了しました。最後にHTTPSでアクセスできることを確認します。
# 環境変数の設定 $ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}') $ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}') # HTTPSでのアクセス # -Hでホストヘッダを書き換える # --resolveで一時的な名前解決を実施 # --cacertで作成した証明書を指定し、ルート証明書として利用させる $ curl -HHost:bookinfo.istio-sample.com --resolve "bookinfo.istio-sample.com:$SECURE_INGRESS_PORT:$INGRESS_HOST" -o /dev/null -w '%{http_code}\n' -s --cacert istio-sample.com.crt "https://bookinfo.istio-sample.com:$SECURE_INGRESS_PORT/productpage"
以上でIngress GatewayでTLSを終端させる設定は完了です。他のセキュリティ機能はあまり使用しない場合でもIngress Gatewayでサーバ証明書を使うケースは多いでしょう。今回は基本となる手順を紹介しましたが、より実践的な手順に関しては、Istio公式ページ(Secure Gateways)を参考にしてください。
本機能はリソース、namespace単位など柔軟に設定できるので、環境や要件に応じた使い方ができます。今回は、下表に示す流れで基本的な設定内容と設定後の通信の違いを確認します。
項番 | 設定/検証内容 | 確認内容 |
---|---|---|
1 | デフォルト状態 | サービスメッシュ内外からの通信が可能であること |
2 | 相互TLS認証設定(STRICT:namespace全体) | サービスメッシュ外からの平文の通信が失敗すること |
3 | 相互TLS認証設定(PERMISSIVE:productpageのみ) | 項番2の設定を上書きし、サービスメッシュ内外から特定のサービス(productpage)への通信が可能であること |
4 | Destination Ruleを組み合わせた相互TLS認証 | サイドカープロキシが存在しないサービスが混在する状況で相互TLS認証での通信が可能であること |
サービス間の通信を暗号化する相互TLS認証について確認します。最初に相互TLS認証を設定していない場合の動作を確認します。2つのSleepアプリケーションからproductpageにHTTPリクエストを発行します。通信が正常に行われた場合は、ステータスコード「200」を返すはずです。
# サービスメッシュ内のPodからproductpageにアクセス $ echo 'from "mesh" to productpage' && kubectl exec -n sample-app "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "mesh" to productgpege 200 # サービスメッシュ外のPodからproductpageにアクセス $ echo 'from "outside mesh" to productpage' && kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "outside mesh" to productpage 200
ここから相互TLS認証設定を実施します。相互TLS認証設定はPeerAuthenticationリソースで設定できます。対象範囲は個別のサービス、namespace、サービスメッシュ全体と用途に応じて設定が可能です。さらに認証モードの指定をすることで通信間の認証の要否を制御できます。
認証モードと認証の要否はこのように整理できます。
相互TLS設定(モード) | メッシュ内からの通信 | メッシュ外からの通信 |
---|---|---|
PERMISSIVE | 相互TLS通信(暗号化通信) | 非暗号化通信 |
STRICT | 相互TLS通信(暗号化通信) | 通信不可 |
DISABLED | 非暗号化通信 | 非暗号化通信 |
PeerAuthenticationリソースを設定していない状態では、サービスメッシュ全体が「PERMISSIVE」モードで動作します。そのため、今回の環境では、サービスメッシュ内の「namespace:sample-app」のアプリケーションから通信する場合は、相互TLS認証が使用されます。一方、サービスメッシュ外の環境においては認証なしで通信します。
ここではPeerAuthenticationリソースを使用して、相互TLS認証の範囲とモードを指定します。現在はサービスメッシュ全体がPERMISSIVEモードなので、「namespace:sample-appには相互TLS認証が必須(STRICTモード)となる」設定を入れます。
・PeerAuthenticationの設定
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: mtls namespace: sample-app # 対象範囲をnamespace全体にする spec: mtls: mode: STRICT # mtlsを必須とさせる設定
準備ができたので、マニフェストファイルをnamespace:sample-appに適用します。
# PeerAuthenticationを適用 $ kubectl apply -f ./peer-authentication.yaml peerauthentication.security.istio.io/mtls created # namespace: sample-appにSTRICTモードのリソースが存在することを確認 $ kubectl get peerauthentications.security.istio.io -n sample-app NAME MODE AGE mtls STRICT 30s
・通信の確認
期待される動作は、次の通りです。
# サービスメッシュ外(namespace:default)からproductpageの通信 $ echo 'from "outside mesh" to productpage' && kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "outside mesh" to productgpege 000 command terminated with exit code 56 # サービスメッシュ内(namespace:sample-app)からproductpageの通信 $ echo 'from "mesh" to productpage' && kubectl exec -n sample-app "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "mesh" to productpage 200
先ほどまで通信できていた、サービスメッシュ外(サイドカープロキシのないPod)からの通信が失敗することを確認できます。これは、相互TLS認証時にクライアント認証ができず、通信を確立できないからです。
先ほどの設定ではnamespace全体に相互TLS認証を強制していました。サービス間の通信をTLS相互認証によって暗号化していますが、前段のIngress Gatewayで暗号化通信を一度終端しており、ユーザーがアクセスするWebページには、サービスの認証は必要としません。ここでは、Webページを表示するproductpageの認証を外した場合を想定してみます。
・PeerAuthenticationの設定
apiVersion: "security.istio.io/v1beta1" kind: "PeerAuthentication" metadata: name: productpage namespace: sample-app spec: selector: matchLabels: app: productpage # 対象をproductpageとする mtls: mode: PERMISSIVE # 相互TLS認証ができない場合は必須としない
# PeerAuthenticationを適用 kubectl apply -f ./peer-authentication_productpage.yaml peerauthentication.security.istio.io/productpage created # 作成したリソースが存在することを確認(PERMISSIVEモード) kubectl get peerauthentications.security.istio.io -n sample-app NAME MODE AGE mtls STRICT 11m productpage PERMISSIVE 7s
・通信の確認
期待される動作は、次の通りです。
# サービスメッシュ外(namespace:default)からproductpageの通信 echo "from no sidecar to productpage" && kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from no sidecar to productpage 200 # サービスメッシュ内(namespace:sample-app)からproductpageの通信 echo 'from "mesh" to productpage' && kubectl exec -n sample-app "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "mesh" to productpage 200
先ほど通信に失敗したサービスメッシュ外(サイドカープロキシのないPod)からの通信が成功することを確認できます。この設定は、「サービスメッシュ全体に相互TLS認証を実施したいが、サービスメッシュ外からのアクセスも存在する」といった要件にも柔軟に対応できるでしょう。
ここまでは、PeerAuthenticationのみを使用して相互TLS認証の要否を設定してきました。先ほどまでのケースでは、相互TLS認証が実施できない場合はエラーを表示しましたが、もっと柔軟に設定したい場合があるかもしれません。
複数バージョンを扱うサービスにおいて、一部のバージョンではサイドカープロキシが存在しないケースを考えてみます。セキュリティ上、常に相互TLS通信を適用させたくても、サイドカープロキシが存在しないので、PeerAuthenticationだけでは制御できません。
上記のように相互TLS認証を使用した通信を規定する場合、Destination Ruleを組み合わせることでより柔軟に対処できます。下表に示す流れでDestination Ruleの設定前と設定後の動作の違いを確認します。
項番 | 設定/検証内容 | 確認内容 |
---|---|---|
1 | PeerAuthenticationをnamespace適用(STRICTモード) | サイドカープロキシが存在しないサービスにルーティングされた場合において、サーバ認証に失敗し、エラーページが表示されること |
2 | Destination Ruleの適用 | サイドカープロキシが存在しないサービスにルーティングされなくなり、相互TLS認証での通信に限定されること |
・reviewsサービスでの事前確認
まずは次の状態を作成します。
# PeerAuthenticationリソースの削除 $ kubectl delete peerauthentications.security.istio.io -n sample-app mtls $ kubectl delete peerauthentications.security.istio.io -n sample-app productpage # 削除されたことの確認(デフォルトはPERMISSIVEモード) kubectl get peerauthentications.security.istio.io -n sample-app No resources found in sample-app namespace. # reviews v2の削除 $ kubectl delete deployment -n sample-app reviews-v2 deployment.apps "reviews-v2" deleted # サイドカーの自動挿入設定を停止 $ kubectl label namespace sample-app istio-injection=disabled --overwrite namespace/sample-app labeled # Bookinfoアプリケーションのデプロイ $ kubectl apply -f ./samples/bookinfo/platform/kube/bookinfo.yaml -n sample-app service/details unchanged serviceaccount/bookinfo-details unchanged deployment.apps/details-v1 unchanged service/ratings unchanged serviceaccount/bookinfo-ratings unchanged deployment.apps/ratings-v1 unchanged service/reviews unchanged serviceaccount/bookinfo-reviews unchanged deployment.apps/reviews-v1 unchanged deployment.apps/reviews-v2 created deployment.apps/reviews-v3 unchanged service/productpage unchanged serviceaccount/bookinfo-productpage unchanged deployment.apps/productpage-v1 unchanged # reviews-v2のREADY欄が1であり、サイドカーが存在しないことを確認 $ kubectl get po -n sample-app NAME READY STATUS RESTARTS AGE details-v1-79f774bdb9-vssjg 2/2 Running 0 3h42m productpage-v1-6b746f74dc-xsptl 2/2 Running 0 3h42m ratings-v1-b6994bb9-4vc4l 2/2 Running 0 3h42m reviews-v1-545db77b95-7wrpz 2/2 Running 0 3h42m reviews-v2-7bf8c9648f-c4wmf 1/1 Running 0 9s reviews-v3-84779c7bbc-574jt 2/2 Running 0 3h42m sleep-557747455f-9vs6v 2/2 Running 0 3h42m
・通信の確認
ここまで準備できたら、ブラウザからBookinfoアプリケーションにアクセスします。設定内容についての詳細を知りたい方は、連載第10回を参考にしてください。
$ kubectl apply -f ./samples/bookinfo/networking/bookinfo-gateway.yaml -n sample-app # リソースの確認 $ kubectl get gateway -n sample-app NAME AGE bookinfo-gateway 12s $ kubectl get virtualservice -n sample-app NAME GATEWAYS HOSTS AGE bookinfo ["bookinfo-gateway"] ["*"] 24s # type: LoadBalancerを使用する場合は以下の環境変数を設定 $ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}') $ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}') # クラスタの外部からBookinfoアプリケーションにアクセス $ curl -s "http://${INGRESS_HOST}:${INGRESS_PORT}/productpage" | grep -o "<title>.*</title>" # ブラウザからアクセスするためにURLを表示 $ echo http://${INGRESS_HOST}:${INGRESS_PORT}/productpage
productpage経由でreviewsに通信を発生させるとv2を含めたサービスにも負荷分散され、正常にページが表示されることを確認できます。これは、現在の設定がデフォルトのPERMISSIVEモードなので、相互TLS認証を使用しない通信も許可されるからです。
PeerAuthenticationリソースで、namespace全体をSTRICTモードにした場合はどうでしょうか。先ほど作成したマニフェストを再度適用します。
・PeerAuthenticationの設定
# PeerAuthenticationを適用 $ kubectl apply -f ./peer-authentication.yaml peerauthentication.security.istio.io/mtls created # namespace: sample-appにSTRICTモードのリソースが存在することを確認 $ kubectl get peerauthentications.security.istio.io -n sample-app NAME MODE AGE mtls STRICT 40s
・通信の確認
先ほどと同様にブラウザからproductpageにアクセスすると、reviews-v2に通信が負荷分散された際はエラーページが表示されます。productpageからreviewsサービスに通信する際に相互TLS認証を必須としているにもかかわらず、reviews-v2ではサイドカープロキシが存在しないので、reviewsサービスのサーバを認証できず、通信が失敗します。
このような環境でもDestination Ruleを組み合わせることでエラーページを表示させないようにすることができます。つまり、Destination Ruleで通信ルールを規定することで、相互TLS認証が可能なサービスのみにルーティングさせることができるので、サイドカーのないreviewsサービスには通信が発生しないようになります。
・Destination Ruleの設定
下記のようにtrafficPolicyを規定することでreviewsサービスへの通信には相互TLS認証を使用することを明示できます。
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: reviews spec: host: reviews trafficPolicy: tls: mode: ISTIO_MUTUAL # mTLSを使用するルールを規定する
マニフェストを適用し、通信を発生させます。
# mTLSを使用するように設定したDestination Ruleの適用 $ kubectl apply destinationRule_reviews.yaml destinationrule.networking.istio.io/reviews created $ kubectl get destinationrules.networking.istio.io -n sample-app NAME HOST AGE reviews reviews 27s
・通信の確認
ブラウザからアクセスするとv2が表示されないはずです。念のため、下記コマンドで10回ほどproductpageからreviewsサービスに対しての通信を発生させてみます。
# productpageからreviewsサービスへの通信 for i in `seq 10` ; do ; ret=`kubectl exec "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -n sample-app -- curl -s http://productpage.sample-app:9080/productpage | grep 'font color=' | tail -n1 | awk '{print $2}'` ; if [ "`echo $ret | grep red`" ] ; then ; echo v3 ; elif [ "`echo $ret | grep black`" ] ; then ; echo v2 ; else echo v1 ; fi ; done v1 v3 v1 v1 v1 v3 v1 v1 v3 v1
同じくv2は表示されません。つまり、Destination Ruleで通信ルールが規定されるので、TLS通信に限定されることが分かります。
このようにPeerAuthenticationとDestination Ruleを組み合わせることで柔軟に相互TLS認証を規定できるので、セキュリティ要件や環境に応じて適用してみるといいでしょう。
本記事では紹介しませんが、その他、ポートごとに相互TLS認証を設定することもできます。詳細については、Istio公式ページ(Authentication Policy)を確認してください。
Istioの相互TLS機能でサービス間通信が暗号化され、第三者からの盗聴を防ぐことができると説明しましたが、パケットはどのようになっているのでしょうか。設定前後において、受信側であるproductpageのサイドカープロキシにパケットキャプチャーを設定し、内容を確認します。
パケットキャプチャーを取得するために、サイドカープロキシを特権コンテナとして利用できるようにインストールします。この設定は、コンテナがホストであるサーバへの高度なアクセス権限を持ち、攻撃者に悪用されるリスクを伴うので、検証やデバッグの際にのみ使用するようにしてください。
# Istioのインストール時に特権コンテナを有効にする(privileged=true) $ istioctl install --set profile=demo --set values.global.proxy.privileged=true -y # 環境変数として、Pod名とIPアドレスをセットする $ productpage=`kubectl get pod -n sample-app -l app=productpage -o jsonpath='{.items[0].metadata.name}'` $ podIP=`kubectl get pod -n sample-app -l app=productpage -o jsonpath='{.items[0].status.podIP}'` # サイドカープロキシでパケットキャプチャーを取得 $ kubectl exec -n sample-app $productpage -c istio-proxy -- sudo tcpdump -vvvv -A -i eth0 dst port 9080 and net $podIP
パケットキャプチャーの準備ができたら、通信を発生させます。
# デフォルト(PERMISSIVE)での確認 kubectl get peerauthentications.security.istio.io -n sample-app No resources found in sample-app namespace. # サービスメッシュ内のPodからproductpageにアクセス $ echo 'from "mesh" to productpage' && kubectl exec -n sample-app "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "mesh" to productpage 200 # サービスメッシュ外のPodからproductpageにアクセス $ echo 'from "outside mesh" to productpage' && kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "outside mesh" to productpage 200
パケットキャプチャーの内容を確認します。サービスメッシュ外からアクセスした場合(サイドカープロキシがない場合)は平文で通信されており、サービスメッシュ内からアクセスした場合(サイドカープロキシがある場合)の通信は暗号化されていることを確認できます。
# パケットキャプチャーの内容(サービスメッシュ内のPodからproductpageにアクセス(サイドカーあり)) 09:18:28.708284 IP (tos 0x0, ttl 63, id 32182, offset 0, flags [DF], proto TCP (6), length 270) 172-16-4-65.sleep.sample-app.svc.cluster.local.45012 > productpage-v1-6b746f74dc-xsptl.9080: Flags [P.], cksum 0x6198 (incorrect -> 0xdd07), seq 0:218, ack 1, win 502, options [nop,nop,TS val 1182729210 ecr 789293290], length 218 E...}.@.?.\....A...6..#x....T.......a...... F.../..............4.......T ...G.....p<.RnB.#z.,|=....+.../...,.0.......?.=..:outbound_.9080_._.productpage.sample-app.svc.cluster.local.......... ...............#..... ...istio-http/1.1.istio.http/1.1........................
相互TLS認証を使用しない設定も確認します。先ほどのマニフェストファイルのモード設定をDISABLEとするだけです。
apiVersion: "security.istio.io/v1beta1" kind: "PeerAuthentication" metadata: name: productpage namespace: sample-app spec: selector: matchLabels: app: productpage # 対象は、productpage mtls: mode: DISABLE # 相互TLS認証を使用しない
リソースをデプロイし、通信を発生させます。
# PeerAuthenticationを適用 kubectl apply -f ./peer-authentication_productpage.yaml peerauthentication.security.istio.io/productpage created # 作成したリソースが存在することを確認(PERMISSIVEモード) kubectl get peerauthentications.security.istio.io -n sample-app NAME MODE AGE mtls STRICT 12m productpage DISABLE 10s # サービスメッシュ内(namespace:sample-app)からproductpageの通信 echo 'from "mesh" to productpage' && kubectl exec -n sample-app "$(kubectl get pod -n sample-app -l app=sleep -o jsonpath={.items..metadata.name})" -- curl http://productpage.sample-app:9080/productpage -o /dev/null -w '%{http_code}\n' -s from "mesh" to productpage 200
パケットキャプチャーの内容を確認します。
# サービスメッシュ内(namespace:sample-app)からproductpageの通信のパケットキャプチャー(DISABLEDモード) 06:57:07.241136 IP (tos 0x0, ttl 63, id 64011, offset 0, flags [DF], proto TCP (6), length 1658) 172-16-4-65.sleep.sample-app.svc.cluster.local.42414 > productpage-v1-6b746f74dc-xsptl.9080: Flags [P.], cksum 0x6704 (incorrect -> 0xeefa), seq 0:1606, ack 1, win 502, options [nop,nop,TS val 1174247743 ecr 780811823], length 1606 E..z..@.?......A...6..#x.9|.........g...... E..?..>/GET /productpage HTTP/1.1 host: productpage.sample-app:9080 user-agent: curl/7.79.1-DEV accept: */* x-forwarded-proto: http x-request-id: f88d7e4d-c822-94e8-a84d-9d6fd70f6720 x-envoy-decorator-operation: productpage.sample-app.svc.cluster.local:9080/* x-envoy-peer-metadata: ChkKDkFQUF9DT05UQUlORVJTEgcaBXNsZWVwChoKCkNMVVNURVJfSUQSDBoKS3ViZXJuZXRlcwoZCg1JU1RJT19WRVJTSU9OEggaBjEuMTEuMArEAQoGTEFCRUxTErkBKrYBCg4KA2FwcBIHGgVzbGVlcAohChFwb2QtdGVtcGxhdGUtaGFzaBIMGgo1NTc3NDc0NTVmCiQKGXNlY3VyaXR5LmlzdGlvLmlvL3Rsc01vZGUSBxoFaXN0aW8KKgofc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtbmFtZRIHGgVzbGVlcAovCiNzZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1yZXZpc2lvbhIIGgZsYXRlc3QKGgoHTUVTSF9JRBIPGg1jbHVzdGVyLmxvY2FsCiAKBE5BTUUSGBoWc2xlZXAtNTU3NzQ3NDU1Zi05dnM2dgoZCglOQU1FU1BBQ0USDBoKc2FtcGxlLWFwcApMCgVPV05FUhJDGkFrdWJlcm5ldGVzOi8vYXBpcy9hcHBzL3YxL25hbWVzcGFjZXMvc2FtcGxlLWFwcC9kZXBsb3ltZW50cy9zbGVlcArSAgoRUExBVEZPUk1fTUVUQURBVEESvAIquQIKQAoUYXp1cmVfY3JlYXRpb25Tb3VyY2USKBomdm1zc2NsaWVudC1ha3MtYWdlbnRwb29sLTEwODU1Nzg1LXZtc3MKHQoOYXp1cmVfbG9jYXRpb24SCxoJamFwYW5lYXN0Ci4KCmF6dXJlX25hbWUSIBoeYWtzLWFnZW50cG9vbC0xMDg1NTc4NS12bXNzXzY5CikKEmF6dXJlX29yY2hlc3RyYXRvchITGhFLdWJlcm5ldGVzOjEuMjEuMQodCg5henVyZV9wb29sTmFtZRILGglhZ2VudHBvb2wKJgoYYXp1cmVfcmVzb3VyY2VOYW1lU3VmZml4EgoaCDEwODU1Nzg1CjQKCmF6dXJlX3ZtSWQSJhokOWU3MTllMDAtZGNiOC00OGJmLTg1ZmEtNWE2MDVmN2RlZGJiChgKDVdPUktMT0FEX05BTUUSBxoFc2xlZXA= x-envoy-peer-metadata-id: sidecar~172.16.4.65~sleep-557747455f-9vs6v.sample-app~sample-app.svc.cluster.local x-envoy-attempt-count: 1 x-b3-traceid: 783afd2de5014b832abc544476021379 x-b3-spanid: 2abc544476021379 x-b3-sampled: 1
先ほどまでは、サイドカーであるPodからの通信を暗号化していましたが、平文での通信となりました。送信元にサイドカープロキシが存在するサービスメッシュ内の通信なので、Envoyの情報がパケットに入っていることも確認できます。
パケットキャプチャーの内容から相互TLS認証機能を使用して通信が暗号化されることを確認できました。興味のある方はぜひ本コラムを参考にパケットの内容を確認してみてください。
ここから3つ目の機能として、JWTでのアクセストークンの検証を見ていきます。JWTの検証はRequestAuthenticationで設定することができ、「jwtRules」で「Issuer」を指定できます。RequestAuthenticationはエンドユーザーの認証に使用されるので、Ingress Gatewayで設定するといいでしょう。
本稿では、以下の流れで設定前後の動作の違いを確認していきます。なおJWT検証は4つ目のセキュリティ機能「アクセス制御機能(認可機能)」を組み合わせることで実現する必要があるので、まずは項番2までを設定し、JWTを検証する動作を確認します。
項番 | 機能分類 | 設定/検証内容 | 確認内容 |
---|---|---|---|
1 | JWT検証(RequestAuthentication) | 1.事前確認 | ・トークンの有無に関係なく、Bookinfoアプリケーションへの通信が可能であること |
2 | 2.RequestAuthenticationの適用 | ・誤ったトークンを用いた場合、Bookinfoアプリケーションへの通信が失敗すること ・トークンがない場合も通信が可能であること |
|
3 | アクセス制御機能(認可機能) | AuthorizationPolicyの適用 | ・誤ったトークンを用いた場合、Bookinfoアプリケーションへの通信が失敗すること ・トークンがない場合も通信が失敗すること |
まずは、事前確認としてトークンの有無に関係なく、Bookinfoアプリケーションへの通信が可能であることを確認します。期待される動作は、次の通りです。
# アクセストークンなしで外部からproductpageに通信 curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -w '%{http_code}\n' -s 200 # アクセストークンあり(誤ったトークン)で外部からproductpageに通信 curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -H "Authorization: Bearer invalidToken" -w '%{http_code}\n' -s 200
ここからRequestAuthenticationによる認証設定を利用して、JWTで認証していきます。
・JWTを使用した認証設定
本記事では、Ingress Gatewayへのアクセスに際して、Auth0で発行したテスト用のトークンを要求するように指定します。Auth0の具体的な説明は割愛しますが、興味ある方は公式ドキュメントを参照してください。
まずはAuth0にログインし、「APIs」の「Menu」から新規APIを作成します。
情報を入力し、「Create」を押下します。今回はサンプルとして、以下のような名前で作成します。
認証APIが出来上がったら「Test」タブを開きます。
作成したAPIを使って認証できるサンプルがあるのでcurlコマンドを使ってトークンを発行します。下記は一例なので、Testタブで確認される情報を使用してトークンを変数に格納してください。
# トークンを発行し、変数に格納する export TOKEN=$(curl --request POST \ --url https://istio-sample.jp.auth0.com/oauth/token \ --header 'content-type: application/json' \ --data '{"client_id":"XXXXXXXXX","client_secret":"XXXXXXXXX","audience":"https://istio-sample.example.com","grant_type":"client_credentials"}' | jq -r .access_token)
トークンが発行されるのを確認できました。このトークンを使って、Istioでの動作を確認します。
Auth0で発行されるトークンをIstioが検証できるようにRequestAuthenticationを設定する必要があります。「issuer」にはJWTの発行元を設定し、「jwksUri」はJWTを検証する公開鍵情報のURIを指定します。issuerはトークンをデコードし、「iss」を確認するとよいでしょう。デコードは、Auth0公式を使うと簡単に確認できます。また、jwksUriはAuth0のドキュメントで確認できます。
yamlに反映すると次のようになります。
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: "jwt-example" namespace: istio-system spec: selector: matchLabels: istio: ingressgateway jwtRules: - issuer: https://istio-sample.jp.auth0.com/ # 発行元を設定 jwksUri: https://istio-sample.jp.auth0.com/.well-known/jwks.json # 公開鍵情報のURIを指定
準備ができたら、マニフェストファイルを適用します。
$ kubectl apply -f ./RequestAuthentication.yaml requestauthentication.security.istio.io/jwt-example created $ kubectl get requestauthentications.security.istio.io -n istio-system NAME AGE jwt-example 17s
・通信の確認
次のパターンで通信してみましょう。
# アクセストークンなしで外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -w '%{http_code}\n' -s 200 # アクセストークンあり(正しいトークン)で外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -H "Authorization: Bearer $TOKEN" -w '%{http_code}\n' -s 200 # アクセストークンあり(誤ったトークン)で外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -H "Authorization: Bearer invalidToken" -w '%{http_code}\n' -s 401
誤ったトークンでは認証に失敗しますが、トークンなしでもアクセスできてしまいました。これは、トークンが正しいものかどうかを検証する設定は入っている一方で、各サービスへのアクセスには利用していないからです。AuthorizationPolicyを使用して、認可の設定を入れます。
ここからは最後の機能であるアクセス制御機能(認可機能)を実践します。Istioではリクエスト元のサービスのアクセス権限を確認した上でリクエストを「許可/拒否する」認可処理をここまで設定してきたJWT検証機能などを組み合わせることで実現できます。
「セキュリティ機能の実践【3】JWT検証」から引き続いた設定として、下表の項番3に示した内容を確認します。
項番 | 機能分類 | 設定/検証内容 | 確認内容 |
---|---|---|---|
1 | JWT検証(RequestAuthentication) | 1.事前確認 | ・トークンの有無に関係なく、Bookinfoアプリケーションへの通信が可能であること |
2 | 2.RequestAuthenticationの適用 | ・誤ったトークンを用いた場合、Bookinfoアプリケーションへの通信が失敗すること ・トークンがない場合、通信が可能であること |
|
3 | アクセス制御機能(認可機能) | 3.AuthorizationPolicyの適用 | ・誤ったトークンを用いた場合、Bookinfoアプリケーションへの通信が失敗すること ・トークンがない場合、通信が失敗すること |
先ほどは、RequestAuthenticationでトークンを使用した認証を設定しました。ここから認可の設定を入れていきます。
・特定のトークンがある場合のみ許可する設定
認可の設定にはAuthorizationPolicyを使用します。該当のトークンを使用した場合のみ、アクセスを許可する設定を入れます。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: "frontend-ingress" namespace: istio-system spec: selector: matchLabels: istio: ingressgateway action: ALLOW # 該当のトークンでのアクセスを許可する設定 rules: # ポリシーのルールを設定 - when: - key: request.auth.audiences values: - https://istio-sample.example.com
作成したリソースを適用します。
$ kubectl apply -f ./AuthorizationPolicy_allow.yaml authorizationpolicy.security.istio.io/frontend-ingress created $ kubectl get authorizationpolicies.security.istio.io -n istio-system NAME AGE frontend-ingress 12s
・通信の確認
先ほどと同じパターンで通信を発生させてみると、トークンなしでアクセスした場合の結果が異なることを確認できます。これは、先ほど設定したポリシーによってIngress Gatewayへのアクセスが拒否されたと判断することができるでしょう。
# アクセストークンなしで外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -w '%{http_code}\n' -s 403 # アクセストークンあり(誤ったトークン)で外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -H "Authorization: Bearer invalidToken" -w '%{http_code}\n' -s 401 # アクセストークンあり(正しいトークン)で外部からproductpageに通信 $ curl http://${INGRESS_HOST}:${INGRESS_PORT}/productpage -o /dev/null -H "Authori zation: Bearer $TOKEN" -w '%{http_code}\n' -s 200
このように、2つのリソースを組み合わせることで、独自に作り込むことなく認証機能を実現できます。公式ページ(JWT Token)にも手順や解説があるので、詳細が知りたい方は参照してください。
ここからは、AuthorizationPolicyを使用してさまざまな認可設定を試します。JWTの有無だけではなく、シンプルなアクセス許可/拒否のポリシーを設定できます。下表に示す流れで基本的な設定を見ていきます。
項番 | 設定/検証内容 | 確認内容 |
---|---|---|
1 | AuthorizationPolicyの設定(namespace、productpage) | ・namespace全体に拒否ポリシー、productpageに許可ポリシーを設定し、外部からproductpageへのアクセスが可能であること ・productpageからdetailsサービスへのアクセスに失敗すること |
2 | AuthorizationPolicyの設定(detailsサービス) | 項番1の状態において、productpage-detailsサービス間での許可ポリシーを設定し、アクセスが成功すること |
まずは、namespace:sample-appに対するリクエストを全て拒否するポリシーを設定します。マニフェスト上でルールが設定されていない場合、対象に対する拒否が設定されます。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: deny-all namespace: sample-app # 対象は、namespace全体 spec: {} # 設定しないと全て「拒否」になる # - {} # 全て「許可」の場合は、左記のように指定
次にproductpageのみにアクセスが可能となるように設定します。AuthorizationPolicyでポリシーを上書きすることによって、個別のサービスへの許可ポリシーが優先されます。
また、下記YAMLではGETメソッドのみが許可されるように設定しています。設定できるルールの詳細は、Istio公式ページ(Authorization Policy)を確認してください。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: productpage-get namespace: sample-app spec: selector: matchLabels: app: productpage # 対象はproductpage action: ALLOW # 許可設定 rules: - to: - operation: methods: ["GET"] # GETメソッドのみ許可
マニフェストを適用し、productpageにアクセスします。ブラウザからの結果が分かりやすいでしょう。次のような状態になるはずです。
$ kubectl apply -f ./AuthorizationPolicy_productpage_get.yaml authorizationpolicy.security.istio.io/productpage-get created $ kubectl get authorizationpolicies.security.istio.io -n sample-app NAME AGE deny-all 12m productpage-get 6s
・通信の確認
productpageのアクセスは許可されましたが、先ほど設定した拒否ポリシーが働いているので、後続のサービス(reviews、details、ratings)にアクセスできないことを示しています。
productpageからのアクセスのみが許可されるポリシーを作成し、動作を確認します。今回は、サービスアカウントを使用してポリシーを設定します。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: "details-viewer" namespace: sample-app spec: selector: matchLabels: app: details # 対象はdetails action: ALLOW # ルールに一致した場合、許可される rules: - from: - source: # リクエストの発行元のサービスアカウントを使用 principals: ["cluster.local/ns/sample-app/sa/bookinfo-productpage"] to: - operation: methods: ["GET"]
マニフェストを適用し、productpageにアクセスします。ブラウザからの結果を確認するといいでしょう。以下のような状態になるはずです。
$ kubectl apply -f ./AuthorizationPolicy_details-viewer.yaml authorizationpolicy.security.istio.io/details-viewer created $ kubectl get authorizationpolicies.security.istio.io -n sample-app NAME AGE deny-all 13m productpage-get 1m details-viewer 10s
・通信の確認
productpageからアクセスできることを確認できました。
このように、基本的にはアクセスを禁止し、必要なものだけをリスト形式で許可することでセキュリティを向上させることができます。
本稿ではマイクロサービスに潜むセキュリティリスクとそれを解決するIstioのセキュリティ機能について解説し、認証や認可の機能を実践しました。
マイクロサービスはさまざまなメリットが得られる一方で新たなセキュリティリスクが生まれます。アプリケーションで実装するだけでなく、Istioを活用して実装してみてください。今回紹介した内容以外にもより細やかなセキュリティ設定も可能ですので、興味ある方はIstio公式ページ(Security)を確認してください。
また、今回で、4回にわたる「サービスメッシュ、Istio」はいったん終わりです。本稿がサービスメッシュおよびIstioを使いこなす一助となれば幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.