PR

コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~

2020年12月15日(火)
田畑 義之
連載5回目となる今回は、Istioを用いたマイクロサービスのシステムをKeycloakを用いて認証強化する手順を紹介します。

第五回からは、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介します。第四回までは、APIセキュリティを考えるうえで最も重要である、外部ネットワークと内部ネットワークの境界部分に着目してハードニング方法を説明してきました(図1)。

図1:第四回までのネットワーク構成

図1:第四回までのネットワーク構成

一方、マイクロサービスでは、多種多様なサービスがAPIを公開し、境界定義が困難を極めるため、たとえシステムの内部通信であっても完全には信用しないという「ゼロトラストネットワーク」の考え方を前提にシステムを構築する必要があります(図2)。

図2:ゼロトラストネットワークを考慮したネットワーク構成

図2:ゼロトラストネットワークを考慮したネットワーク構成

第五回から数回に分けて、このゼロトラストネットワークの考え方を前提としたマイクロサービスシステムにおいて、Istio、Quarkus、Strimziといったマイクロサービスの注目技術とKeycloakを組み合わせて、システムを堅牢化する方法を説明します。

今回はマイクロサービスシステムを取り上げるため、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図3)。

図3:システム構成

図3:システム構成

使用する各製品のバージョンを表1に示します。なお、QuarkusとStrimziについては次回以降に説明します。

表1:使用する製品とバージョン

製品バージョン
Minikubev1.13.1
  (Kubernetes)v1.19.2
  (Docker)19.03.8
Keycloak11.0.3
Istio1.7.4

なお、Kubernetes環境(Minikube)はドキュメントなどを参考にすでにインストールされ、動作しているものとします。

Kubernetes上へのKeycloakのデプロイ

まずはKeycloakをKubernetes上にデプロイしましょう。今回は、Keycloakクイックスタートリポジトリのkubernetes-exampleを使います。

リスト1: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の管理コンソールにアクセスしてみましょう。

リスト2: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による暗号化は割愛します。

図4:Keycloakログイン画面

図4:Keycloakログイン画面

IstioとKeycloak

Istioとは、マイクロサービス間の複雑なネットワーク(いわゆるサービスメッシュ)を管理するためのプラットフォームです。Istioを使用することで、マイクロサービス間の通信を暗号化や認証認可によってセキュアにすることができます。ここではIstioとKeycloakを組み合わせた堅牢化方法をご紹介します(図5)。

図5:IstioとKeycloakを用いたBookinfoアプリケーションの堅牢化

図5:IstioとKeycloakを用いたBookinfoアプリケーションの堅牢化

Keycloakが払い出したアクセストークンを用いて外部アプリがAPIコールするところまでは、前回同様です。その後のシステム内(サービス間)の通信を、IstioのコンポーネントであるEnvoyが仲介し、相互TLS化とJWT(JSON Web Token)によるアクセス制御によって堅牢化します。

IstioのインストールとBookinfoアプリケーションのデプロイ

まずはIstioをインストールし、Bookinfoアプリケーションをデプロイしましょう。

リスト3: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アプリケーションにアクセスしてみましょう。

リスト4: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)。

図6:BookinfoアプリケーションのProductページ

図6:BookinfoアプリケーションのProductページ

Istioのサービスメッシュの可視化などを行うKialiのダッシュボードにもアクセスしてみましょう。

リスト5: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)。

図7:Kialiダッシュボード

図7:Kialiダッシュボード

各サービス間の通信を相互TLS化

実際に堅牢化していきます。まずは、各サービス間の通信を相互TLS(mutual TLS: mTLS)にしましょう(図8)。

図8:相互TLSによるサービス間通信の暗号化

図8:相互TLSによるサービス間通信の暗号化

Bookinfoアプリケーションのディレクトリに相互TLSを実現するDestinationRuleのサンプルがありますので、それを参考にDestinationRule(destination-rule-all-mtls-v2.yaml)を作成しましょう。

リスト6:設定ファイルのサンプルをコピー

$ 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

……
---
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を適用します。

リスト8:DestinationRuleの適用

$ kubectl apply -f destination-rule-all-mtls-v2.yaml

さらに、以下のようなPeerAuthentication(peer-authentication-mtls.yaml)を作成します。この設定によって、各サービス間の通信を相互TLSのみに限定することができます。

リスト9:peer-authentication-mtls.yaml

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: mtls
spec:
  mtls:
    mode: STRICT

作成したPeerAuthenticationを適用します。

リスト10:PeerAuthenticationの適用

$ kubectl apply -f peer-authentication-mtls.yaml

以上で、各サービス間の通信を相互TLSにすることができました。BookinfoアプリケーションのProductページにアクセスしてみましょう(図9)。各サービス間の通信は相互TLSになりましたが、変わりなく動作していることを確認できます。

図9:BookinfoアプリケーションのProductページ

図9:BookinfoアプリケーションのProductページ

各サービス間の通信をJWTでアクセス制御

次に、各サービス間の通信をJWTでアクセス制御しましょう(図10)。ゼロトラストネットワークなので、マイクロサービスシステムではJWT(アクセストークン)を各サービスに伝播し、各サービスにおいてJWTを検証することが推奨されます。

図10:JWTによるアクセス制御

図10:JWTによるアクセス制御

アクセストークンを伝播する方法としては、1つのアクセストークンを使い回す方法と、Token Exchange(RFC8693)を用いる方法が考えられます(図11)。両者には一長一短ありますが、一般的に外部アプリのエンドユーザが伝播先のサービス(図11でいうところのService B)を認識できるのであれば前者を、認識できないのであれば後者を選択する傾向にあります。ここでは実装が簡単なため、外部アプリのエンドユーザがすべてのサービスを認識できると仮定し、1つのアクセストークンを使い回す方法を選択します。

図11:アクセストークンを伝播する2つの方法

図11:アクセストークンを伝播する2つの方法

Bookinfoアプリケーションは、デフォルトの実装ではProductページ以降のサービスにアクセストークンを伝播させることができませんので、まずはアクセストークンをProductページ以降のサービスに伝播できるよう、Bookinfoアプリケーションの実装を変更しましょう。

リスト11:Bookinfoアプリケーションの実装を変更

$ git clone https://github.com/istio/istio.git -b 1.7.4 --depth 1

Productページのソース(istio/samples/bookinfo/src/productpage/productpage.py)を変更し、Authorizationヘッダを伝播させることで、アクセストークンを伝播するようにします。

リスト12:productpage.pyを変更

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ヘッダを伝播させることで、アクセストークンを伝播するようにします。

リスト13:LibertyRestEndpoint.javaを変更

……
    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アプリケーションをビルドします。

リスト14: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)に設定します。

リスト15:作成したイメージをDeploymentに設定

……
---
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を適用します。

リスト16:Deploymentを適用

$ kubectl apply -f istio-1.7.4/samples/bookinfo/platform/kube/bookinfo.yaml

以上で、アクセストークンをProductページ以降のサービスに伝播できるようになりました。次に、外部アプリがアクセストークンを発行できるようにするために、Keycloakのリソースを作成します(表2)。

表2:作成するKeycloakのリソース

リソース設定項目名設定値備考
レルムNamebookinfo
クライアントClient IDsample_application外部アプリ用クライアント
 Access Typeconfidential
 Standard Flow EnabledOFF
 Direct Access Grants EnabledON簡単のため、Resource Owner Password Credentials Grantを使う
ユーザUsernamesample_user

以上で、以下のようなコマンドでアクセストークンを発行できるようになりました。

リスト17:アクセストークンを発行するコマンド

$ 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チェックや署名チェックといった、アクセストークンの静的チェックを各サービスで実施するよう設定できます。

リスト18:request-authentication-keycloak.yaml

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を適用します。

リスト19:RequestAuthenticationの適用

$ kubectl apply -f request-authentication-keycloak.yaml

さらに、以下のようなAuthorizationPolicy(authorization-policy-keycloak.yaml)を作成します。この設定によって、各サービス間の通信にJWTの使用を強制することができます。

リスト20:authorization-policy-keycloak.yaml

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を適用します。

リスト21:AuthorizationPolicyの適用

$ kubectl apply -f authorization-policy-keycloak.yaml

以上で、各サービス間の通信にJWTによるアクセス制御を追加できました。アクセストークンを付与せずに、または無効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしようとすると、Productページが表示されないこと(「RBAC: access denied」などが表示されること)を確認できます(図12)。

図12:BookinfoアプリケーションのProductページ(Access Denied)

図12:BookinfoアプリケーションのProductページ(Access Denied)

一方、有効なアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図13)。

図13:BookinfoアプリケーションのProductページ

図13:BookinfoアプリケーションのProductページ

上で作成したAuthorizationPolicyでは、Issuerチェックのような全サービス共通の簡単なチェックのみを定義しましたが、より詳細なアクセス制御も定義可能です。例えば、前回ご紹介したアクセストークンのaud(Audience)クレームを用いたアクセス制御方法は、マイクロサービスシステムのようなアクセストークンを各サービスに伝播するシステムでは導入を強く推奨されますが、こちらもAuthorizationPolicyで実現できます。

audクレームを用いたアクセス制御方法は、すべてのサービスに導入することが望ましいですが、ここでは例としてProductページサービスとDetailsサービスを用いて実現方法を説明します。まずは、必要なKeycloakのリソースを作成します(表3)。

表3:作成するKeycloakのリソース

リソース設定項目名設定値備考
クライアントClient IDproduct_pageProductページサービス用クライアント
 Access Typebearer-only
クライアントClient IDdetailsDetailsサービス用クライアント
 Access Typebearer-only
クライアントスコープNameproduct_pageProductページサービス用クライアントスコープ
 プロトコルマッパーNameproduct_page-audience
 Mapper TypeAudience
 Included Client Audienceproduct_page
 Add to access tokenON
クライアントスコープNamedetailsDetailsサービス用クライアントスコープ
 プロトコルマッパーNamedetails-audience
 Mapper TypeAudience
 Included Client Audiencedetails
 Add to access tokenON
クライアントClient IDsample_applicationクライアントスコープの設定を追加する
 Optional Client Scopesproduct_page
  details 

次に、Productページサービスのaudクレームチェック用に、以下のようなAuthorizationPolicy(authorization-policy-product-page-aud.yaml)を作成します。

リスト22: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)を作成します。

リスト23: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を適用します。

リスト24: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)。

図14:BookinfoアプリケーションのProductページ(Access Denied)

図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)

図15:BookinfoアプリケーションのProductページ(DetailsサービスAccess Denied)

次に、scopeにproduct_pageとdetailsを指定して発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Details部分も表示されることを確認できます(図16)。

図16:BookinfoアプリケーションのProductページ

図16:BookinfoアプリケーションのProductページ

次回は、コンテナ上のマイクロサービスの認証強化について、QuarkusとKeycloakを組み合わせて、システムを堅牢化する方法を説明します。

株式会社 日立製作所
OSSソリューションセンタにて、API管理や認証周りのOSSの開発/サポート/普及活動に従事。3scaleおよびkeycloakコミュニティのコントリビュータであり、多数のコードをコミットしている。

連載バックナンバー

セキュリティ技術解説
第5回

コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~

2020/12/15
連載5回目となる今回は、Istioを用いたマイクロサービスのシステムをKeycloakを用いて認証強化する手順を紹介します。
セキュリティ技術解説
第4回

Keycloakを用いたハードニングの実装方法

2020/9/24
連載4回目となる今回は、Keycloakを用いたハードニングの実装方法を紹介します。
セキュリティ技術解説
第3回

APIセキュリティのハードニング

2020/7/22
連載3回目となる今回は、前回紹介したシステムに対する攻撃のタイプを紹介し、APIセキュリティのハードニングに則った対応策による堅牢化の手法を紹介します。

Think IT会員サービス無料登録受付中

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスの概要とメリットをチェック

他にもこの記事が読まれています