第五回からは、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介します。第四回までは、APIセキュリティを考えるうえで最も重要である、外部ネットワークと内部ネットワークの境界部分に着目してハードニング方法を説明してきました(図1)。
図1:第四回までのネットワーク構成
一方、マイクロサービスでは、多種多様なサービスがAPIを公開し、境界定義が困難を極めるため、たとえシステムの内部通信であっても完全には信用しないという「ゼロトラストネットワーク」の考え方を前提にシステムを構築する必要があります(図2)。
図2:ゼロトラストネットワークを考慮したネットワーク構成
第五回から数回に分けて、このゼロトラストネットワークの考え方を前提としたマイクロサービスシステムにおいて、Istio、Quarkus、Strimziといったマイクロサービスの注目技術とKeycloakを組み合わせて、システムを堅牢化する方法を説明します。
今回はマイクロサービスシステムを取り上げるため、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図3)。
図3:システム構成
使用する各製品のバージョンを表1に示します。なお、QuarkusとStrimziについては次回以降に説明します。
表1:使用する製品とバージョン
製品 | バージョン |
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を使います。
リスト1:kubernetes-example
1 | $ kubectl create namespace keycloak |
Keycloakの管理コンソールにアクセスしてみましょう。
リスト2:Keycloakの管理コンソールにアクセス
3 | $ echo $(kubectl get services/keycloak -o go-template='{{(index .spec.ports 0).nodePort}} |
上記コマンド結果より、管理コンソールのURLは、http://172.30.10.93:31385/auth/adminであることがわかります。ブラウザでアクセスすると、ログイン画面が表示されます(図4)。kubernetes-exampleでは、初期管理ユーザとして、ユーザ名admin、パスワードadminのユーザが作られますので、このユーザを利用して管理コンソールにログインすることができます。なお、ここでは簡単のため、前回ご紹介した外部ネットワークと内部ネットワークの境界部分におけるTLSによる暗号化は割愛します。
図4:Keycloakログイン画面
IstioとKeycloak
Istioとは、マイクロサービス間の複雑なネットワーク(いわゆるサービスメッシュ)を管理するためのプラットフォームです。Istioを使用することで、マイクロサービス間の通信を暗号化や認証認可によってセキュアにすることができます。ここではIstioとKeycloakを組み合わせた堅牢化方法をご紹介します(図5)。
図5:IstioとKeycloakを用いたBookinfoアプリケーションの堅牢化
Keycloakが払い出したアクセストークンを用いて外部アプリがAPIコールするところまでは、前回同様です。その後のシステム内(サービス間)の通信を、IstioのコンポーネントであるEnvoyが仲介し、相互TLS化とJWT(JSON Web Token)によるアクセス制御によって堅牢化します。
IstioのインストールとBookinfoアプリケーションのデプロイ
まずはIstioをインストールし、Bookinfoアプリケーションをデプロイしましょう。
リスト3:IstioとBookinfoアプリケーション
2 | $ export PATH="$PATH:/root/istio-1.7.4/bin" |
3 | $ istioctl install --set profile=demo |
4 | $ kubectl label namespace default istio-injection=enabled |
5 | $ kubectl apply -f istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml |
6 | $ kubectl apply -f istio-1.7.4/samples/bookinfo/networking/bookinfo-gateway.yaml |
7 | $ kubectl apply -f istio-1.7.4/samples/addons |
8 | $ kubectl patch svc -n istio-system kiali -p '{"spec": {"type": "LoadBalancer"}}' |
Bookinfoアプリケーションにアクセスしてみましょう。
リスト4:Bookinfoアプリケーションにアクセス
3 | $ echo $(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}') |
上記コマンド結果より、BookinfoアプリケーションのURLは、http://172.30.10.93:30986/productpageであることがわかります。ブラウザでアクセスすると、BookinfoアプリケーションのProductページが表示されます(図6)。
図6:BookinfoアプリケーションのProductページ
Istioのサービスメッシュの可視化などを行うKialiのダッシュボードにもアクセスしてみましょう。
リスト5:Kialiのダッシュボードにアクセス
3 | $ echo $(kubectl -n istio-system get service kiali -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}') |
上記コマンド結果より、KialiダッシュボードのURLは、http://172.30.10.93:31097/kialiであることがわかります。ブラウザでアクセスすると、Kialiダッシュボードが表示されます(図7)。
図7:Kialiダッシュボード
各サービス間の通信を相互TLS化
実際に堅牢化していきます。まずは、各サービス間の通信を相互TLS(mutual TLS: mTLS)にしましょう(図8)。
図8:相互TLSによるサービス間通信の暗号化
Bookinfoアプリケーションのディレクトリに相互TLSを実現するDestinationRuleのサンプルがありますので、それを参考にDestinationRule(destination-rule-all-mtls-v2.yaml)を作成しましょう。
リスト6:設定ファイルのサンプルをコピー
1 | $ 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を実現できます。
リスト7:destination-rule-all-mtls-v2.yaml
03 | apiVersion: networking.istio.io/v1alpha3 |
24 | version: v2-mysql-vm} // ここまで削除 |
26 | apiVersion: networking.istio.io/v1alpha3 |
作成したDestinationRuleを適用します。
リスト8:DestinationRuleの適用
1 | $ kubectl apply -f destination-rule-all-mtls-v2.yaml |
さらに、以下のようなPeerAuthentication(peer-authentication-mtls.yaml)を作成します。この設定によって、各サービス間の通信を相互TLSのみに限定することができます。
リスト9:peer-authentication-mtls.yaml
1 | apiVersion: security.istio.io/v1beta1 |
2 | kind: PeerAuthentication |
作成したPeerAuthenticationを適用します。
リスト10:PeerAuthenticationの適用
1 | $ kubectl apply -f peer-authentication-mtls.yaml |
以上で、各サービス間の通信を相互TLSにすることができました。BookinfoアプリケーションのProductページにアクセスしてみましょう(図9)。各サービス間の通信は相互TLSになりましたが、変わりなく動作していることを確認できます。
図9:BookinfoアプリケーションのProductページ
各サービス間の通信をJWTでアクセス制御
次に、各サービス間の通信をJWTでアクセス制御しましょう(図10)。ゼロトラストネットワークなので、マイクロサービスシステムではJWT(アクセストークン)を各サービスに伝播し、各サービスにおいてJWTを検証することが推奨されます。
図10:JWTによるアクセス制御
アクセストークンを伝播する方法としては、1つのアクセストークンを使い回す方法と、Token Exchange(RFC8693)を用いる方法が考えられます(図11)。両者には一長一短ありますが、一般的に外部アプリのエンドユーザが伝播先のサービス(図11でいうところのService B)を認識できるのであれば前者を、認識できないのであれば後者を選択する傾向にあります。ここでは実装が簡単なため、外部アプリのエンドユーザがすべてのサービスを認識できると仮定し、1つのアクセストークンを使い回す方法を選択します。
図11:アクセストークンを伝播する2つの方法
Bookinfoアプリケーションは、デフォルトの実装ではProductページ以降のサービスにアクセストークンを伝播させることができませんので、まずはアクセストークンをProductページ以降のサービスに伝播できるよう、Bookinfoアプリケーションの実装を変更しましょう。
リスト11:Bookinfoアプリケーションの実装を変更
Productページのソース(istio/samples/bookinfo/src/productpage/productpage.py)を変更し、Authorizationヘッダを伝播させることで、アクセストークンを伝播するようにします。
リスト12:productpage.pyを変更
1 | def getForwardHeaders(request): |
3 | incoming_headers = ['x-request-id', 'x-datadog-trace-id', 'x-datadog-parent-id', 'x-datadog-sampled', 'Authorization'] // Authorizationを追加 |
Reviewsのソース(istio/samples/bookinfo/src/reviews/reviews-application/src/main/java/application/rest/LibertyRestEndpoint.java)を変更し、Authorizationヘッダを伝播させることで、アクセストークンを伝播するようにします。
リスト13:LibertyRestEndpoint.javaを変更
2 | private final static String[] headers_to_propagate = {"x-request-id","x-b3-traceid","x-b3-spanid","x-b3-sampled","x-b3-flags", |
3 | "x-ot-span-context","x-datadog-trace-id","x-datadog-parent-id","x-datadog-sampled", "end-user","user-agent", "Authorization"}; // Authorizationを追加 |
Bookinfoアプリケーションをビルドします。
リスト14:Bookinfoアプリケーションをビルド
1 | $ 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)に設定します。
リスト15:作成したイメージをDeploymentに設定
16 | image: docker.io/istio/examples-bookinfo-reviews-v1:2.0.0 |
32 | image: docker.io/istio/examples-bookinfo-reviews-v2:2.0.0 |
48 | image: docker.io/istio/examples-bookinfo-reviews-v3:2.0.0 |
66 | image: docker.io/istio/examples-bookinfo-productpage-v1:2.0.0 |
変更したDeploymentを適用します。
リスト16:Deploymentを適用
1 | $ kubectl apply -f istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml |
以上で、アクセストークンをProductページ以降のサービスに伝播できるようになりました。次に、外部アプリがアクセストークンを発行できるようにするために、Keycloakのリソースを作成します(表2)。
表2:作成するKeycloakのリソース
リソース | 設定項目名 | 設定値 | 備考 |
レルム | 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 | ― |
以上で、以下のようなコマンドでアクセストークンを発行できるようになりました。
次に、以下のようなRequestAuthentication(request-authentication-keycloak.yaml)を作成します。この設定によって、Issuerチェックや署名チェックといった、アクセストークンの静的チェックを各サービスで実施するよう設定できます。
リスト18:request-authentication-keycloak.yaml
01 | apiVersion: security.istio.io/v1beta1 |
02 | kind: RequestAuthentication |
12 | forwardOriginalToken: true |
作成したRequestAuthenticationを適用します。
リスト19:RequestAuthenticationの適用
1 | $ kubectl apply -f request-authentication-keycloak.yaml |
さらに、以下のようなAuthorizationPolicy(authorization-policy-keycloak.yaml)を作成します。この設定によって、各サービス間の通信にJWTの使用を強制することができます。
リスト20:authorization-policy-keycloak.yaml
01 | apiVersion: security.istio.io/v1beta1 |
02 | kind: AuthorizationPolicy |
09 | - key: request.auth.claims[iss] |
作成したAuthorizationPolicyを適用します。
リスト21:AuthorizationPolicyの適用
1 | $ kubectl apply -f authorization-policy-keycloak.yaml |
以上で、各サービス間の通信にJWTによるアクセス制御を追加できました。アクセストークンを付与せずに、または無効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしようとすると、Productページが表示されないこと(「RBAC: access denied」などが表示されること)を確認できます(図12)。
図12:BookinfoアプリケーションのProductページ(Access Denied)
一方、有効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図13)。
図13:BookinfoアプリケーションのProductページ
上で作成したAuthorizationPolicyでは、Issuerチェックのような全サービス共通の簡単なチェックのみを定義しましたが、より詳細なアクセス制御も定義可能です。例えば、前回ご紹介したアクセストークンのaud(Audience)クレームを用いたアクセス制御方法は、マイクロサービスシステムのようなアクセストークンを各サービスに伝播するシステムでは導入を強く推奨されますが、こちらもAuthorizationPolicyで実現できます。
audクレームを用いたアクセス制御方法は、すべてのサービスに導入することが望ましいですが、ここでは例としてProductページサービスとDetailsサービスを用いて実現方法を説明します。まずは、必要なKeycloakのリソースを作成します(表3)。
表3:作成するKeycloakのリソース
リソース | 設定項目名 | 設定値 | 備考 |
クライアント | 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)を作成します。
リスト22:authorization-policy-product-page-aud.yaml
01 | apiVersion: security.istio.io/v1beta1 |
02 | kind: AuthorizationPolicy |
04 | name: product-page-aud |
12 | - key: request.auth.claims[aud] |
13 | notValues: ["product_page"] |
次に、Detailsサービスのaudクレームチェック用に、以下のようなAuthorizationPolicy(authorization-policy-details-aud.yaml)を作成します。
リスト23:authorization-policy-details-aud.yaml
01 | apiVersion: security.istio.io/v1beta1 |
02 | kind: AuthorizationPolicy |
12 | - key: request.auth.claims[aud] |
13 | notValues: ["details"] |
作成したAuthorizationPolicyを適用します。
リスト24:AuthorizationPolicyの適用
1 | $ kubectl apply -f authorization-policy-product-page-aud.yaml |
2 | $ kubectl apply -f authorization-policy-details-aud.yaml |
以上で、ProductページサービスとDetailsサービスに対するaudクレームを用いたアクセス制御を設定できました。
試しに、scopeにproduct_pageとdetailsを指定せずに発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしようとすると、Productページが表示されないこと(「RBAC: access denied」が表示されること)を確認できます(図14)。
図14:BookinfoアプリケーションのProductページ(Access Denied)
次に、scopeにproduct_pageのみを指定して発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページは表示されますが、Details部分(左半分)が表示されないこと(「Sorry, product details are currently unavailable for this book.」が表示されること)を確認できます(図15)。
図15:BookinfoアプリケーションのProductページ(DetailsサービスAccess Denied)
次に、scopeにproduct_pageとdetailsを指定して発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Details部分も表示されることを確認できます(図16)。
図16:BookinfoアプリケーションのProductページ
次回は、コンテナ上のマイクロサービスの認証強化について、QuarkusとKeycloakを組み合わせて、システムを堅牢化する方法を説明します。