コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~
第五回からは、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介します。第四回までは、APIセキュリティを考えるうえで最も重要である、外部ネットワークと内部ネットワークの境界部分に着目してハードニング方法を説明してきました(図1)。
一方、マイクロサービスでは、多種多様なサービスがAPIを公開し、境界定義が困難を極めるため、たとえシステムの内部通信であっても完全には信用しないという「ゼロトラストネットワーク」の考え方を前提にシステムを構築する必要があります(図2)。
第五回から数回に分けて、このゼロトラストネットワークの考え方を前提としたマイクロサービスシステムにおいて、Istio、Quarkus、Strimziといったマイクロサービスの注目技術とKeycloakを組み合わせて、システムを堅牢化する方法を説明します。
今回はマイクロサービスシステムを取り上げるため、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図3)。
使用する各製品のバージョンを表1に示します。なお、QuarkusとStrimziについては次回以降に説明します。
製品 | バージョン |
---|---|
Minikube | v1.13.1 |
(Kubernetes) | v1.19.2 |
(Docker) | 19.03.8 |
Keycloak | 11.0.3 |
Istio | 1.7.4 |
なお、Kubernetes環境(Minikube)はドキュメントなどを参考にすでにインストールされ、動作しているものとします。
Kubernetes上へのKeycloakのデプロイ
まずはKeycloakをKubernetes上にデプロイしましょう。今回は、Keycloakクイックスタートリポジトリのkubernetes-exampleを使います。
$ kubectl create namespace keycloak $ wget -q -O - https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/latest/kubernetes-examples/keycloak.yaml | sed "s/default/keycloak/" | kubectl create -f - -n keycloak
Keycloakの管理コンソールにアクセスしてみましょう。
$ echo $(minikube ip) 172.30.10.93 $ echo $(kubectl get services/keycloak -o go-template='{{(index .spec.ports 0).nodePort}} ' -n keycloak) 31385
上記コマンド結果より、管理コンソールのURLは、http://172.30.10.93:31385/auth/adminであることがわかります。ブラウザでアクセスすると、ログイン画面が表示されます(図4)。kubernetes-exampleでは、初期管理ユーザとして、ユーザ名admin、パスワードadminのユーザが作られますので、このユーザを利用して管理コンソールにログインすることができます。なお、ここでは簡単のため、前回ご紹介した外部ネットワークと内部ネットワークの境界部分におけるTLSによる暗号化は割愛します。
IstioとKeycloak
Istioとは、マイクロサービス間の複雑なネットワーク(いわゆるサービスメッシュ)を管理するためのプラットフォームです。Istioを使用することで、マイクロサービス間の通信を暗号化や認証認可によってセキュアにすることができます。ここではIstioとKeycloakを組み合わせた堅牢化方法をご紹介します(図5)。
Keycloakが払い出したアクセストークンを用いて外部アプリがAPIコールするところまでは、前回同様です。その後のシステム内(サービス間)の通信を、IstioのコンポーネントであるEnvoyが仲介し、相互TLS化とJWT(JSON Web Token)によるアクセス制御によって堅牢化します。
IstioのインストールとBookinfoアプリケーションのデプロイ
まずはIstioをインストールし、Bookinfoアプリケーションをデプロイしましょう。
$ curl -L https://istio.io/downloadIstio | sh - $ export PATH="$PATH:/root/istio-1.7.4/bin" $ istioctl install --set profile=demo $ kubectl label namespace default istio-injection=enabled $ kubectl apply -f istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml $ kubectl apply -f istio-1.7.4/samples/bookinfo/networking/bookinfo-gateway.yaml $ kubectl apply -f istio-1.7.4/samples/addons $ kubectl patch svc -n istio-system kiali -p '{"spec": {"type": "LoadBalancer"}}'
Bookinfoアプリケーションにアクセスしてみましょう。
$ echo $(minikube ip) 172.30.10.93 $ echo $(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}') 30986
上記コマンド結果より、BookinfoアプリケーションのURLは、http://172.30.10.93:30986/productpageであることがわかります。ブラウザでアクセスすると、BookinfoアプリケーションのProductページが表示されます(図6)。
Istioのサービスメッシュの可視化などを行うKialiのダッシュボードにもアクセスしてみましょう。
$ echo $(minikube ip) 172.30.10.93 $ echo $(kubectl -n istio-system get service kiali -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}') 31097
上記コマンド結果より、KialiダッシュボードのURLは、http://172.30.10.93:31097/kialiであることがわかります。ブラウザでアクセスすると、Kialiダッシュボードが表示されます(図7)。
各サービス間の通信を相互TLS化
実際に堅牢化していきます。まずは、各サービス間の通信を相互TLS(mutual TLS: mTLS)にしましょう(図8)。
Bookinfoアプリケーションのディレクトリに相互TLSを実現するDestinationRuleのサンプルがありますので、それを参考にDestinationRule(destination-rule-all-mtls-v2.yaml)を作成しましょう。
$ cp istio-1.7.4/samples/bookinfo/networking/destination-rule-all-mtls.yaml destination-rule-all-mtls-v2.yaml
destination-rule-all-mtls-v2.yamlファイルを以下のように変更します。ここでspec.trafficPolicy.tls.modeに指定しているISTIO_MUTUALでは、Istioが自動生成した証明書を用いるため、自身で証明書などを準備する手間がなく、非常に簡単に相互TLSを実現できます。
…… --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: ratings spec: host: ratings trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 - name: v2 // 以下9行を削除 labels: version: v2 - name: v2-mysql labels: version: v2-mysql - name: v2-mysql-vm labels: version: v2-mysql-vm} // ここまで削除 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: details spec: host: details trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v1 labels: version: v1 - name: v2 // 以下3行を削除 labels: version: v2 // ここまで削除 ---
作成したDestinationRuleを適用します。
$ kubectl apply -f destination-rule-all-mtls-v2.yaml
さらに、以下のようなPeerAuthentication(peer-authentication-mtls.yaml)を作成します。この設定によって、各サービス間の通信を相互TLSのみに限定することができます。
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: mtls spec: mtls: mode: STRICT
作成したPeerAuthenticationを適用します。
$ kubectl apply -f peer-authentication-mtls.yaml
以上で、各サービス間の通信を相互TLSにすることができました。BookinfoアプリケーションのProductページにアクセスしてみましょう(図9)。各サービス間の通信は相互TLSになりましたが、変わりなく動作していることを確認できます。
各サービス間の通信をJWTでアクセス制御
次に、各サービス間の通信をJWTでアクセス制御しましょう(図10)。ゼロトラストネットワークなので、マイクロサービスシステムではJWT(アクセストークン)を各サービスに伝播し、各サービスにおいてJWTを検証することが推奨されます。
アクセストークンを伝播する方法としては、1つのアクセストークンを使い回す方法と、Token Exchange(RFC8693)を用いる方法が考えられます(図11)。両者には一長一短ありますが、一般的に外部アプリのエンドユーザが伝播先のサービス(図11でいうところのService B)を認識できるのであれば前者を、認識できないのであれば後者を選択する傾向にあります。ここでは実装が簡単なため、外部アプリのエンドユーザがすべてのサービスを認識できると仮定し、1つのアクセストークンを使い回す方法を選択します。
Bookinfoアプリケーションは、デフォルトの実装ではProductページ以降のサービスにアクセストークンを伝播させることができませんので、まずはアクセストークンをProductページ以降のサービスに伝播できるよう、Bookinfoアプリケーションの実装を変更しましょう。
$ git clone https://github.com/istio/istio.git -b 1.7.4 --depth 1
Productページのソース(istio/samples/bookinfo/src/productpage/productpage.py)を変更し、Authorizationヘッダを伝播させることで、アクセストークンを伝播するようにします。
def getForwardHeaders(request): …… incoming_headers = ['x-request-id', 'x-datadog-trace-id', 'x-datadog-parent-id', 'x-datadog-sampled', 'Authorization'] // Authorizationを追加 …… return headers
Reviewsのソース(istio/samples/bookinfo/src/reviews/reviews-application/src/main/java/application/rest/LibertyRestEndpoint.java)を変更し、Authorizationヘッダを伝播させることで、アクセストークンを伝播するようにします。
…… private final static String[] headers_to_propagate = {"x-request-id","x-b3-traceid","x-b3-spanid","x-b3-sampled","x-b3-flags", "x-ot-span-context","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampled", "end-user","user-agent", "Authorization"}; // Authorizationを追加 ……
Bookinfoアプリケーションをビルドします。
$ istio/samples/bookinfo/src/build-services.sh 2.0.0 docker.io/istio
ビルドして作成したイメージをProductページ、DetailsのDeployment(istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml)に設定します。
…… --- apiVersion: apps/v1 kind: Deployment metadata: name: reviews-v1 …… spec: …… template: …… spec: …… containers: - name: reviews image: docker.io/istio/examples-bookinfo-reviews-v1:2.0.0 …… --- apiVersion: apps/v1 kind: Deployment metadata: name: reviews-v2 …… spec: …… template: …… spec: …… containers: - name: reviews image: docker.io/istio/examples-bookinfo-reviews-v2:2.0.0 …… --- apiVersion: apps/v1 kind: Deployment metadata: name: reviews-v3 …… spec: …… template: …… spec: …… containers: - name: reviews image: docker.io/istio/examples-bookinfo-reviews-v3:2.0.0 …… --- …… --- apiVersion: apps/v1 kind: Deployment metadata: name: productpage-v1 …… spec: …… template: …… spec: …… containers: - name: productpage image: docker.io/istio/examples-bookinfo-productpage-v1:2.0.0 …… ---
変更したDeploymentを適用します。
$ kubectl apply -f istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml
以上で、アクセストークンをProductページ以降のサービスに伝播できるようになりました。次に、外部アプリがアクセストークンを発行できるようにするために、Keycloakのリソースを作成します(表2)。
リソース | 設定項目名 | 設定値 | 備考 |
---|---|---|---|
レルム | Name | bookinfo | ― |
クライアント | Client ID | sample_application | 外部アプリ用クライアント |
Access Type | confidential | ― | |
Standard Flow Enabled | OFF | ― | |
Direct Access Grants Enabled | ON | 簡単のため、Resource Owner Password Credentials Grantを使う | |
ユーザ | Username | sample_user | ― |
以上で、以下のようなコマンドでアクセストークンを発行できるようになりました。
$ curl http://172.30.10.93:31385/auth/realms/bookinfo/protocol/openid-connect/token -d "grant_type=password&username=sample_user&password=<password>&client_id=sample_application&client_secret=<client secret>&scope=openid"
次に、以下のようなRequestAuthentication(request-authentication-keycloak.yaml)を作成します。この設定によって、Issuerチェックや署名チェックといった、アクセストークンの静的チェックを各サービスで実施するよう設定できます。
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: keycloak spec: jwtRules: - issuer: http://172.30.10.93:31385/auth/realms/bookinfo jwksUri: http://172.30.10.93:31385/auth/realms/bookinfo/protocol/openid-connect/certs fromHeaders: - name: Authorization prefix: "Bearer " forwardOriginalToken: true
作成したRequestAuthenticationを適用します。
$ kubectl apply -f request-authentication-keycloak.yaml
さらに、以下のようなAuthorizationPolicy(authorization-policy-keycloak.yaml)を作成します。この設定によって、各サービス間の通信にJWTの使用を強制することができます。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: keycloak spec: action: ALLOW rules: - when: - key: request.auth.claims[iss] values: ["http://172.30.10.93:31385/auth/realms/bookinfo"]
作成したAuthorizationPolicyを適用します。
$ kubectl apply -f authorization-policy-keycloak.yaml
以上で、各サービス間の通信にJWTによるアクセス制御を追加できました。アクセストークンを付与せずに、または無効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしようとすると、Productページが表示されないこと(「RBAC: access denied」などが表示されること)を確認できます(図12)。
一方、有効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図13)。
上で作成したAuthorizationPolicyでは、Issuerチェックのような全サービス共通の簡単なチェックのみを定義しましたが、より詳細なアクセス制御も定義可能です。例えば、前回ご紹介したアクセストークンのaud(Audience)クレームを用いたアクセス制御方法は、マイクロサービスシステムのようなアクセストークンを各サービスに伝播するシステムでは導入を強く推奨されますが、こちらもAuthorizationPolicyで実現できます。
audクレームを用いたアクセス制御方法は、すべてのサービスに導入することが望ましいですが、ここでは例としてProductページサービスとDetailsサービスを用いて実現方法を説明します。まずは、必要なKeycloakのリソースを作成します(表3)。
リソース | 設定項目名 | 設定値 | 備考 |
---|---|---|---|
クライアント | Client ID | product_page | Productページサービス用クライアント |
Access Type | bearer-only | ― | |
クライアント | Client ID | details | Detailsサービス用クライアント |
Access Type | bearer-only | ― | |
クライアントスコープ | Name | product_page | Productページサービス用クライアントスコープ |
プロトコルマッパー | Name | product_page-audience | ― |
Mapper Type | Audience | ― | |
Included Client Audience | product_page | ― | |
Add to access token | ON | ― | |
クライアントスコープ | Name | details | Detailsサービス用クライアントスコープ |
プロトコルマッパー | Name | details-audience | ― |
Mapper Type | Audience | ― | |
Included Client Audience | details | ― | |
Add to access token | ON | ― | |
クライアント | Client ID | sample_application | クライアントスコープの設定を追加する |
Optional Client Scopes | product_page | ― | |
details |
次に、Productページサービスのaudクレームチェック用に、以下のようなAuthorizationPolicy(authorization-policy-product-page-aud.yaml)を作成します。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: product-page-aud spec: selector: matchLabels: app: productpage action: DENY rules: - when: - key: request.auth.claims[aud] notValues: ["product_page"]
次に、Detailsサービスのaudクレームチェック用に、以下のようなAuthorizationPolicy(authorization-policy-details-aud.yaml)を作成します。
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: details-aud spec: selector: matchLabels: app: details action: DENY rules: - when: - key: request.auth.claims[aud] notValues: ["details"]
作成したAuthorizationPolicyを適用します。
$ kubectl apply -f authorization-policy-product-page-aud.yaml $ kubectl apply -f authorization-policy-details-aud.yaml
以上で、ProductページサービスとDetailsサービスに対するaudクレームを用いたアクセス制御を設定できました。
試しに、scopeにproduct_pageとdetailsを指定せずに発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしようとすると、Productページが表示されないこと(「RBAC: access denied」が表示されること)を確認できます(図14)。
次に、scopeにproduct_pageのみを指定して発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページは表示されますが、Details部分(左半分)が表示されないこと(「Sorry, product details are currently unavailable for this book.」が表示されること)を確認できます(図15)。
次に、scopeにproduct_pageとdetailsを指定して発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Details部分も表示されることを確認できます(図16)。
次回は、コンテナ上のマイクロサービスの認証強化について、QuarkusとKeycloakを組み合わせて、システムを堅牢化する方法を説明します。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~
- コンテナ上のマイクロサービスの認証強化 ~QuarkusとKeycloak~
- BookinfoデモでIstioを体感する
- Keycloakを用いたハードニングの実装方法
- APIセキュリティのハードニング
- FAPI 1.0に準拠したクライアントアプリケーションと リソースサーバの作り方
- クライアントポリシーを利用したKeycloakの設定方法と、FAPIリファレンス実装の紹介
- NGINX Ingress Controllerの柔軟なアプリケーション制御、具体的なユースケースと設定方法を理解する
- Keycloakのインストールと構築例
- FAPIとKeycloakの概要