Istioが解決するマイクロサービスの認証・認可
はじめに
マイクロサービスの数が増えるにつれて発生する問題の1つに認証・認可があります。外部からの通信はもちろん、マイクロサービス間の通信についても無制限に許可されているとセキュリティのリスクが高まります。
そこで、マイクロサービスに認証・認可を実装することが必要となります。
しかし、マイクロサービスの開発では複数のチームがそれぞれの実装言語を使用して開発を行うことが可能です。そのため、認証・認可の仕組みを提供するにはマイクロサービス共通の仕様をまとめて、それぞれの言語で実装する必要があります。
この方法では、マイクロサービスを構築する度に認証・認可の仕組みを実装する必要があり、仕様に変更があれば全てのマイクロサービスで修正する必要があります。
サービスメッシュを利用することで、この課題を解決できます。サービスメッシュは各マイクロサービスのプロキシとして動作し、クライアントとサーバー間の通信を保護するPEP(Policy Enforcement Point)として機能します。
本記事では、サービスメッシュの1つであるIstioを使ったマイクロサービスの認証・認可について説明します。また、JWT Tokenを使ってマイクロサービス間の通信を制御する方法をハンズオン形式で紹介します。
Istioの認証・認可
Istioの認証・認可のアーキテクチャから紹介します。
Authentication
Istioは、PeerAuthenticationとRequestAuthenticationの2つの認証方法を提供します。
これらのポリシーが適用されると、Istioは対象となるエンドポイントに対して設定を非同期で送信します。
リクエストを送信するクライアントは適用されたポリシーを満たす必要があります。
ポリシーはクラスタ全体や特定のnamespace・podに対して設定することが可能です。
PeerAuthentication
PeerAuthenticationでは、mTLSが許可または要求されるかどうかを設定します。
Port単位でmTLSを強制するかどうか指定可能になっています。
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: foo
spec:
selector:
matchLabels:
app: finance
mtls:
mode: STRICT
portLevelMtls:
8080:
mode: DISABLE
RequestAuthentication
RequestAuthenticationでは、リクエストに含まれるJWT Tokenを検証します。
ただし、JWT Tokenを含まないリクエストには適用されないため後述するAuthorizationPolicyを使ってJWT Tokenを強制できます。また、JWTは複数指定できるため異なるプロバイダーからのJWTを受け入れることが可能になっています。
JWKsを取得するクライアントはIstiodとEnvoyから選択可能です。デフォルトでは、20分の間隔でIstiodが取得します。
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: httpbin
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
jwtRules:
- issuer: "issuer-foo"
jwksUri: https://example.com/.well-known/jwks.json
RequestAuthenticationは、最終的にEnvoyのJWT Filterに変換されます。
Authorization
Istioの認可ポリシーは、各istio-proxyの認可エンジンによって評価されALLOW・DENYのどちらかに決定します。
認可ポリシーは、AuthorizationPolicyによって設定可能です。
ポリシーはクラスタ全体や特定のnamespace・podに対して設定することが可能です。
AuthorizationPolicyは最終的にEnvoyのHTTP Filter、Listener Filterに変換されます。
AuthorizationPolicy
AuthorizationPolicyでは、actionやrulesの設定が可能です。
actionには、CUSTOM、DENY、ALLOWを設定可能です。また、複数のポリシーが設定されている場合にCUSTOM、DENY、ALLOWの順番で評価されます。
rulesには、from(送信元)、to(宛先)、when(追加の条件)の3つの項目があります。
この例では、dev namespaceのリソースに対して有効なJWT Tokenを持つGETリクエストを許可します。
一部のルールを使用するには、mTLSの有効化が必須となっています。
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: foo
spec:
selector:
matchLabels:
app: httpbin
version: v1
action: ALLOW
rules:
- from:
- source:
namespaces: ["dev"]
to:
- operation:
methods: ["GET"]
when:
- key: request.auth.claims[iss]
values: ["https://accounts.google.com"]
JWT Tokenを使った認証・認可
ここからは、実際にIstioの認証・認可を手を動かしながら学んでいきます。
事前準備
KubernetesクラスタとIstioの構築を行います。また、JWT Tokenの発行にKeycloakを使用します。
KINDでKubernetesクラスタを作成
kind create cluster --name istio-demo --config cluster.yaml
Ingress GatewayをNodePortを使って外部に公開するためextraPortMappings
を設定しています。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
- role: worker
image: kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865
extraPortMappings:
- containerPort: 30081
hostPort: 30081
Istioをインストール
istioctlを使ってIstioをインストールします。
curl -L https://istio.io/downloadIstio | sh -
cd istio-1.23.0
export PATH=$PWD/bin:$PATH
istioctl install
Ingress Gatewayを公開
istio-ingressgatewayを修正します。
kubectl -n istio-system edit svc istio-ingressgateway
apiVersion: v1
kind: Service
metadata:
name: istio-ingressgateway
namespace: istio-system
ports:
- name: status-port
nodePort: 31042
port: 15021
protocol: TCP
targetPort: 15021
- name: http2
nodePort: 30080 <- kindでworker nodeに指定したPortに変更
port: 80
protocol: TCP
targetPort: 8080
- name: https
nodePort: 30930
port: 443
protocol: TCP
targetPort: 8443
type: NodePort <- LoadBalancerから変更
Keycloakをインストール
公式ドキュメントに従ってインストールします。
kubectl create -f https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/latest/kubernetes/keycloak.yaml
Istioのルーティング設定
Keycloakの管理画面にアクセスできるようにGatewayとVirtualServiceをデプロイします。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: public-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: public-virtualservice
spec:
hosts:
- "*"
gateways:
- public-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: keycloak
port:
number: 8080
Keycloakの設定
Keycloakの管理画面にアクセスしてrealmとclientを作成します。
http://localhost:30081
手順は公式ドキュメントに従います。
RequestAuthenticationとAuthorizationPolicyをデプロイする
ここからは、実際にRequestAuthenticationとAuthorizationPolicyを使って認証・認可を実装します。
RequestAuthentication
RequestAuthenticationには、Keycloakの公開鍵のパスを設定します。この値は、http://localhost:30081/realms/myrealm/.well-known/openid-configuration
のjwks_uri
から取得できます。
issuer
には、事前準備で作成したmyrealm
を設定します。
forwardOriginalToken
は、istio-proxyのバックエンドサービスにJWT Tokenを送信するかどうかを設定します。
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: service-a
namespace: service-a
spec:
selector:
matchLabels:
app: service-a
jwtRules:
- issuer: "http://localhost:30081/realms/myrealm"
jwksUri: http://keycloak.default.svc.cluster.local:8080/realms/myrealm/protocol/openid-connect/certs
forwardOriginalToken: true
AuthorizationPolicy
ここでは、/service-a/auth
へのリクエストに対してDENYアクションを設定しています。
追加条件として、issuer
がhttp://localhost:30081/realms/myrealm
ではない場合にDENYします。
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: service-a
namespace: service-a
spec:
selector:
matchLabels:
app: service-a
action: DENY
rules:
- to:
- operation:
paths: ["/service-a/auth"]
when:
- key: request.auth.claims[iss]
notValues: ["http://localhost:30081/realms/myrealm"]
アプリケーション
istio-proxyのバックエンドサービスをGoを使って構築します。このアプリケーションでは、2つのパスを提供します。
/service-a/auth
にリクエストする際にはJWT Tokenが必須になっています。
/service-a/no-auth
へのリクエストにはJWT Tokenは不要になっています。
package main
import (
"fmt"
"net/http"
)
func authHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the auth path.")
}
func noAuthHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the no-auth path.")
}
func main() {
http.HandleFunc("/service-a/auth", authHandler)
http.HandleFunc("/service-a/no-auth", noAuthHandler)
fmt.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Error starting server:", err)
}
}
今回は、上記のアプリケーションをghcrにuploadして利用します。
docker build -t ghcr.io/<username>/istio-demo:v0.0.1
docker push ghcr.io/<username>/istio-demo:v0.0.1
では、Deploymentを使ってアプリケーションをデプロイします。
apiVersion: v1
kind: Namespace
metadata:
labels:
istio-injection: enabled
name: service-a
---
apiVersion: v1
kind: Service
metadata:
name: service-a
namespace: service-a
spec:
selector:
app: service-a
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-a
namespace: service-a
spec:
replicas: 1
selector:
matchLabels:
app: service-a
template:
metadata:
labels:
app: service-a
spec:
containers:
- name: service-a
image: ghcr.io/<username>/istio-demo:v0.0.1 # usernameを修正
ports:
- containerPort: 8080
Istioのルーティング設定
/service-a
に到達できるようにVirtualServiceを修正します。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: public-virtualservice
spec:
hosts:
- "*"
gateways:
- public-gateway
http:
### 追記
- match:
- uri:
prefix: /service-a
route:
- destination:
host: service-a.service-a.svc.cluster.local
port:
number: 8080
###
- match:
- uri:
prefix: /
route:
- destination:
host: keycloak
port:
number: 8080
動作確認
まずは、有効なJWT TokenをKeycloakから取得します。事前準備で作成したClientを使ってTokenを発行します。
> curl http://localhost:30081/realms/myrealm/protocol/openid-connect/token -d "grant_type=password&username=myuser&password=12345&client_id=myclient&client_secret=GMf7klyDB07lf7BRlh8VUypNhihmlea0&scope=openid"
では、実際にリクエストしてみます。
> curl http://localhost:30081/service-a/no-auth
This is the no-auth path.%
> curl http://localhost:30081/service-a/auth
RBAC: access denied%
> curl -H "Authorization: Bearer xxx" http://localhost:30081/service-a/auth
This is the auth path.%
/service-a/auth
はJWT Tokenが無い場合にRBAC: access denied%
となることを確認できました。
今回はクライアントにcurlを利用しましたがマイクロサービス間の通信でも同様にJWT Tokenを付与することでリクエストが許可されます。
最後に
いかがだったでしょうか?
Istioを活用することでマイクロサービスの認証・認可を簡単に実装できました。
今回は紹介できませんでしたが、EnvoyFilterを使って認証・認可を外部のサービスに委譲することもできます。
ぜひ、試してみてください。
Discussion