第5回 Open Policy Agent とサイドカーパターンによる認可の実装

| 11 min read
Author: shigeki-shoji shigeki-shojiの画像

庄司です。

前回の記事で、「挨拶の音声を生成する」コマンド (以降 Hello コマンドまたは Hello サービスといいます) を完成させました。

この記事では、このコマンドの実行権限チェックに Open Policy Agent (OPA) を使って説明します。

図のようにサービスは、3つのコンテナイメージで構成された、docker-compose または Pod です。

ゼロトラストアーキテクチャ (ZTA)

米国国立標準技術研究所 (NIST) が発行している「Zero Trust Architecture (NIST SP 800-207)」と、この記事の構成の対応関係は次のようになります。

ZTA

JWT 認証

#

OpenID Connect で使用される ID Token は RFC 7515 で標準化されている JWS (JSON Web Signature) ですが、一般的には JWT と呼ばれているので、この記事中も JWT とします。

JWT は . (ドット) で区切られた3つの部分から構成され、ヘッダ (header)、ペイロード (payload)、署名 (signature) をそれぞれ URL Safe な Base64 でエンコードした文字列です。

ヘッダには、署名検証のための暗号化アルゴリズム (alg)、公開鍵を特定するためのキー ID (kid) 等を含んでいます。ペイロードには、トークンの発行者 (iss)、クライアント ID (aud)、有効期限 (exp) などの他に任意の属性値を含められます。

ID Token の検証では、正しい発行者 (iss) とクライアント ID (aud) によるトークンかの判断と有効期限 (exp) 内かを判断し、発行者から取得した公開鍵で署名を検証して改ざんされていない正当なトークンであるかを判断します。

Envoy Proxy の JWT Authentication フィルタを使用すると、JWT を検証し、不当な場合 401 Unauthorized (デフォルト) をレスポンスします。

妥当な場合、フィルタはペイロード部分をフォワードするリクエストの HTTP Header に追加もできます。Hello コマンドは、HTTP Header へ payload の追加を前提にしています。

サンプルのフィルタの設定部分は次のとおりです。

    - name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
keycloak:
issuer: "https://keycloak.example.com/auth/realms/passengers"
audiences:
- "hibernation-pod"
forward_payload_header: payload
remote_jwks:
http_uri:
uri: "https://keycloak.example.com/auth/realms/passengers/protocol/openid-connect/certs"
cluster: jwks
timeout: 5s
cache_duration: 600s
rules:
- match:
prefix: /example/hibernation-pod
requires:
provider_name: keycloak

発行者 (iss) を issuer に設定し、クライアント ID (aud) を audiences に設定します。また、公開鍵取得のためにアクセスする URI を remote_jwks の http_uri に設定します。

2023年1月5日追記

JWKS のエンドポイントから取得される公開鍵は RFC 7517 で標準化されている JSON 配列フォーマットです。JSON 形式の公開鍵から Java の PublicKey を生成するコードサンプルが RSA のみですが「OpenID Connect のメモ」にあります。

ID Token を必要としない、つまり認証が不要な API はこのフィルタをスキップさせるため、rules でフィルタが必要なパスのプレフィックス (prefix) を設定します。

External Authorization

#

この記事で実装した Hello コマンドは、ID Token のペイロードに含まれる custom:typeHuman の場合に実行が許可されます。このポリシー適用に OPA を利用します。

Envoy Proxy の External Authorization フィルタを使用して、Envoy Proxy と OPA 間で gRPC/HTTP2 を使って通信する認可処理が可能になります。

Envoy Proxy と OPA 間のインターフェースは Protocol Buffers (protobuf) で定義されています。インターフェースが一致していれば OPA 以外も使用可能で、Red Hat の Authorino はその1つです。

この処理の中で HTTP Header の追加 ("response_headers_to_add") や削除 ("request_headers_to_remove")、呼び出し先のパスの書き換え等も可能です。

ポリシーは Rego というドメイン固有言語 (DSL) で記述します。

Envoy Proxy から送られてくるリクエストを Rego でアクセスする方法の詳細は、Go 言語のソースコード (request.go) を参照してください。

準備

#

Rego 言語で書いたコードのテストのために、OPA をインストールします。

macOS の場合は Homebrew を使います。

brew install opa

コード

#

Hello コードを実行できるのは Human です。次のようなコードにしました。

package envoy.authz

import input.attributes.request.http as http_request

default allow = false

payload := payload {
jwt_payload := http_request.headers["payload"]
payload := json.unmarshal(base64url.decode(jwt_payload))
}

allow := action_allowed {
re_match("^\/example\/hibernation-pod\/.*\/hello.*$", http_request.path)
payload["custom:type"] == "Human"
action_allowed := {
"allowed": true
}
}

テストコード は次のとおりです。

package envoy.authz

test_post_allowed {
allow with input as { "attributes": {
"request": {
"http": {
"path": "/example/hibernation-pod/id-001/hello",
"headers": {
"payload": "eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiY3VzdG9tOmZpcnN0bmFtZSI6IkphbWVzIiwiYXVkIjoiQVBQQ0xJRU5USUQiLCJleHAiOjE2NTQ3NTg3NTcsImN1c3RvbTp0eXBlIjoiSHVtYW4ifQ==" }
}
}
}
}
}

カレントディレクトリを sidecar/opa にして opa test . -v コマンドでテストします。

opa test . -v
example-policy-test.rego:
data.envoy.authz.test_post_allowed: PASS (6.21519ms)
--------------------------------------------------------------------------------
PASS: 1/1
Information

OPA の Rego で、パスを変えたい場合、レスポンスの headers:path にパスを設定します。

{
"allowed": true,
"headers": {
":path": "/index.html"
}
}

削除したい Header がある場合は次のようなレスポンスを返します。

{
"allowed": true,
"request_headers_to_remove": ["payload"]
}

サンプルのフィルタの設定部分は次のとおりです。

    - name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
grpc_service:
envoy_grpc:
cluster_name: authz
timeout: 0.250s

コンテナビルド

#

Spring Boot は、Gradle の場合には bootBuildImage を実行してコンテナイメージをビルドできます。

Information

Maven を使っている場合は、spring-boot:build-image でコンテナイメージをビルドできます。
詳しくは「Spring Boot Docker」を参照してください。

./gradlew bootBuildImage --imageName=example

GitHub Actions を使ってビルドしたイメージが GitHub Packages にあります。

Docker Compose の例

#

Docker Compose は、以前は docker コマンドとは別の docker-compose というコマンドでしたが、今では docker コマンドで run などの代わりに compose をタイプして実行できます。

Docker Compose で実行する場合の docker-compose.yaml は次のようになりました。

version: "3"
services:
envoy:
image: envoyproxy/envoy:v1.21-latest
volumes:
- ./envoy/front-envoy.yaml:/etc/front-envoy.yaml
ports:
- 8081:8081
command: ["-c", "/etc/front-envoy.yaml", "--service-cluster", "front-proxy"]
opa:
image: openpolicyagent/opa:latest-envoy
volumes:
- ./opa/config.yaml:/work/config.yaml
- ./opa/example-policy.rego:/work/example-policy.rego
command: ["run", "--server", "--log-level", "debug", "-c", "/work/config.yaml", "/work/example-policy.rego"]
example:
image: ghcr.io/edward-mamezou/example:v0.6.0
volumes:
- ~/.aws:/home/cnb/.aws
- ./application.yaml:/workspace/application.yaml
- ./tmp:/tmp
environment:
AWS_REGION: ap-northeast-1

sidecar/envoy/front-envoy.yamlsidecar/application.yaml ファイルの作成は、利用している OpenID Connect の IdP (Identity Provider) 等の環境に合わせてください。

Kubernetes の例

#

この記事では、このサービスの動作が確認できる最低限の説明にとどめます。

Docker Compose の場合と同じく sidecar/envoy/front-envoy.yamlsidecar/application.yaml ファイルの作成は、利用している OpenID Connect の IdP (Identity Provider) 等の環境に合わせてください。

Kubernetes は Docker よりセキュリティが向上しています。同一 Pod 内のコンテナ間の通信には、ローカル・ループバック・アドレス (127.0.0.1) が使われます。このため front-envoy.yaml で設定される Envoy Proxy が Hello サービス (example) や OPA へのアクセスに使用する IP Address は 127.0.0.1 となります。

設定ファイルのサンプルが、Docker Compose 用の sidecar/envoy/front-envoy-docker.yaml.example と Pod 用の sidecar/envoy/front-envoy-pod.yaml.example の2つ用意したのはそのためです。

Kubernetes に設定ファイルをマウントするために、次のコマンドで ConfigMap と Secret を作成します。AWS のクレデンシャル、Hello サービスのポリシー、IdP のシークレットが設定されるファイルは ConfigMap ではなく、セキュアな Secret を使用します。

kubectl create configmap proxy-config --from-file envoy/front-envoy.yaml
kubectl create secret generic aws --from-file ~/.aws/credentials
kubectl create secret generic opa-policy --from-file opa/example-policy.rego --from-file opa/config.yaml
kubectl create secret generic spring-config --from-file application.yaml

Pod をデプロイします。

kubectl apply -f deployment.yaml

deployment.yaml は次のとおりです。

kind: Deployment
apiVersion: apps/v1
metadata:
name: example-app
labels:
app: example-app
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
containers:
- name: example
image: ghcr.io/edward-mamezou/example:v0.6.0
env:
- name: AWS_REGION
value: ap-northeast-1
volumeMounts:
- readOnly: true
mountPath: /home/cnb/.aws
name: aws
- readOnly: true
mountPath: /workspace/config
name: spring-config
- name: envoy
image: envoyproxy/envoy:v1.21-latest
ports:
- containerPort: 8081
volumeMounts:
- readOnly: true
mountPath: /config
name: proxy-config
args:
- "-c"
- "/config/front-envoy.yaml"
- "--service-cluster"
- "example-proxy"
- name: opa
image: openpolicyagent/opa:latest-envoy
volumeMounts:
- readOnly: true
mountPath: /policy
name: opa-policy
args:
- "run"
- "--server"
- "--log-level"
- "debug"
- "-c"
- "/policy/config.yaml"
- "/policy/example-policy.rego"
volumes:
- name: proxy-config
configMap:
name: proxy-config
- name: opa-policy
secret:
secretName: opa-policy
- name: aws
secret:
secretName: aws
- name: spring-config
secret:
secretName: spring-config

ブラウザからアクセスできるように、port-foward を実行します。

kubectl port-forward deployment/example-app --address 0.0.0.0 80:8081

ブラウザからは、http://localhost/ 等でアクセスできます。

認証認可処理について

#

伝統的なシステムは認証に集中型の製品が使われ、リバースプロキシを介して HTTP Header にユーザーIDが設定され、サービスそれぞれに独自の認可処理が組み込まれてきました。

これは、侵入者が認証サービスやリバースプロキシを突破しない限り、機密情報にアクセスできない境界モデル (perimeter model) であることを意味しています。プライベートネットワーク内の信頼されたリソースと、インターネット等外部の信頼されないリソースの間に壁を築き、プライベートネットワーク内のシステムは HTTP Header などに設定されたユーザーIDを信頼して認可処理してきました。

認可処理については、早い段階から設計される場合もあれば、ドメインロジックの開発の遅い段階で着手する場合などさまざまでした。

多くの場合、この認可要件はドメインロジックに組み込まれ、認可要件の変更への迅速な対応を困難にしてきました。

これらも要因となり、侵入者がひとたびこのようなプライベートネットワーク内のリソースへの足がかりができれば、すべてのサービスにアクセスできます。

この記事の説明で、認可に OPA を採用し Kubernetes の場合にはポリシーをセキュアな Secret に保存して利用しました。

JWT は署名によって改ざんを検出できます。プライベートネットワークにある各サービスは、JWT によって認証情報を確認し、ドメインロジックと分離された OPA により認可されたアクセスのみを受け入れることで、セキュリティを向上できます。

まとめ

#

OpenAPI Generator と Spring Boot を使ってマイクロサービスを構築する場合のさまざまな懸念点、課題の解決策をシリーズを通して説明してきました。

シリーズは、今回のこの記事で一区切りとなります。

これまでの記事を振り返ります。

第1回は、OpenAPI Generator とはどういうものかの概要を説明しました。

第2回は、マイクロサービスの設計では欠かせない「ドメイン駆動設計」のためのイベントストーミングを紹介し、ドメイン駆動設計の戦略的設計の概要を説明しました。

第3回は、OpenAPI Generator のようなコード生成を活用する場合に重要となる Generation Gap パターンについて説明しました。

第4回は、ドメイン駆動設計の戦略的設計、戦術的設計を利用して、OpenAPI Generator と Spring Boot でサービスを完成させました。

今回

#

今回の記事で、セキュリティ向上のためドメインに組み込まなかった認証認可をサイドカーパターンで実現し、全体のサービスを完成しました。

この記事のコード全体は GitHub リポジトリ にあります。

2023年1月27日追記

Kubernetes 環境に Keycloak をインストールする場合 Helm チャートが使用できます。また Apple Silicon にも対応しています。詳細なインストール手順は「Apple Touch ID Keyboard を使ったパスワードレス認証」を参照してください。

参考

#

Keycloak を IdP として実行する場合は、次の記事も参照してください。

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. ChatGPTのベースになった自然言語処理モデル「Transformer」を調べていたら「Hugging Face」に行き着いた (2023-03-20)
  2. ChatGPTに自然言語処理モデル「GPT2-Japanese」の使用方法を聞きながら実装したら想像以上に優秀だった件 (2023-03-22)
  3. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  4. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  5. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  6. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  7. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  8. 直感が理性に大反抗!「モンティ・ホール問題」 (2022-07-04)
  9. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  10. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)