Go言語を使って、JWTを検証するコードを実装した記事の続きです。
[Golang] OIDCのトークンを検証するJWTの検証をバックエンドサーバで実装すると、冗長的な構成となるため、外出しできるならばすべきコンポーネントです。そこで今回は、サービスメッシュで知られているIstioを使って、JWT検証を外出ししたいと思います。
- JWTの検証をIstioで行いたい
- Kuberentes環境でJWTの検証を考えている
さっそく動かしてみましょう。
TOC
概要
Istio Ingress Gateway上でJWTの検証を行いバックエンドサーバにHTTPのヘッダーとしてアクセストークンを渡すように設定します。
デモ用の設定
公式ページにあるデモシナリオをベースに設定を行います。
Istioの初期設定などは割愛するのでこちらを見て各自設定して下さい。
参考 End-user authenticationデモ用ネームスペースの作成とPodのデプロイ
kubectl create ns foo
kubectl apply -f <(istioctl kube-inject -f samples/httpbin/httpbin.yaml) -n foo
httpbinアプリケーションをingress gatewayを通して外部に公開
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: httpbin-gateway
namespace: foo
spec:
selector:
istio: ingressgateway # use Istio default gateway implementation
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin
namespace: foo
spec:
hosts:
- "*"
gateways:
- httpbin-gateway
http:
- route:
- destination:
port:
number: 8000
host: httpbin.foo.svc.cluster.local
EOF
現在の設定では、JWTの検証をせずアプリケーションにアクセスできるため、下記のようになります。
$ curl -vvv "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Fri, 31 Dec 2021 15:07:30 GMT
< content-type: application/json
< content-length: 604
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 2
<
{
"headers": {
"Accept": "*/*",
"Host": "192.168.11.203",
"User-Agent": "curl/7.68.0",
"X-B3-Parentspanid": "e2be66890773a81c",
"X-B3-Sampled": "0",
"X-B3-Spanid": "d76f992ad96ca98d",
"X-B3-Traceid": "22e71ef0f1a426b4e2be66890773a81c",
"X-Envoy-Attempt-Count": "1",
"X-Envoy-External-Address": "100.96.1.1",
"X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=dc4427aa53abd305005efead810490c543a3757ae1f44529b918b5eea50c7558;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"
}
}
* Connection #0 to host 192.168.11.203 left intact
JWT検証の設定
JWTの検証を追加するため、RequestAuthentication
を設定します。
kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: "jwt-example"
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
jwtRules:
- issuer: "testing@secure.istio.io"
jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.12/security/tools/jwt/samples/jwks.json"
outputPayloadToHeader: x-verified-jwt
EOF
また、アクセストークンをHTTPヘッダー内に含めるためoutputPayloadToHeader: x-verified-jwt
を公式の設定に追加しています。
この状態で同じようにアクセスすると成功します。
$ curl -vvv "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Fri, 31 Dec 2021 15:12:24 GMT
< content-type: application/json
< content-length: 604
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 1
<
{
"headers": {
"Accept": "*/*",
"Host": "192.168.11.203",
"User-Agent": "curl/7.68.0",
"X-B3-Parentspanid": "9cc5ae6982caafce",
"X-B3-Sampled": "0",
"X-B3-Spanid": "55fe95acbedf45d1",
"X-B3-Traceid": "f5c4ed38600ebd779cc5ae6982caafce",
"X-Envoy-Attempt-Count": "1",
"X-Envoy-External-Address": "100.96.1.1",
"X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=dc4427aa53abd305005efead810490c543a3757ae1f44529b918b5eea50c7558;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"
}
}
* Connection #0 to host 192.168.11.203 left intact
理由は、トークンが設定されている場合には検証するがトークンが設定されていない場合には許可する動作になっているためです。ここにトークンを必須とするAuthorizationPolicy
を追加します。
kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: "frontend-ingress"
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ["*"]
to:
- operation:
paths: ["/headers"]
EOF
上記の設定では、/headers
にアクセスした場合正当なトークンが必須となるように設定しています。
この状態で同様のリクエストを投げてみます。
$ curl -vvv "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< content-length: 19
< content-type: text/plain
< date: Fri, 31 Dec 2021 15:16:53 GMT
< server: istio-envoy
<
* Connection #0 to host 192.168.11.203 left intact
RBAC: access denied
期待通り403 Forbidden
のエラーを返しています。
では、正当なトークンを含めてリクエストを投げてみます。
TOKEN=$(curl https://raw.githubusercontent.com/istio/istio/release-1.12/security/tools/jwt/samples/demo.jwt -s)
curl -vvv --header "Authorization: Bearer $TOKEN" "$INGRESS_HOST:$INGRESS_PORT/headers"
動作結果は以下のとおりです。
$ curl -vvv --header "Authorization: Bearer $TOKEN" "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Fri, 31 Dec 2021 15:18:14 GMT
< content-type: application/json
< content-length: 779
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 1
<
{
"headers": {
"Accept": "*/*",
"Host": "192.168.11.203",
"User-Agent": "curl/7.68.0",
"X-B3-Parentspanid": "35bd40578428b910",
"X-B3-Sampled": "0",
"X-B3-Spanid": "be200c9a3f24e29e",
"X-B3-Traceid": "098f4d98f820863e35bd40578428b910",
"X-Envoy-Attempt-Count": "1",
"X-Envoy-External-Address": "100.96.1.1",
"X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=dc4427aa53abd305005efead810490c543a3757ae1f44529b918b5eea50c7558;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account",
"X-Verified-Jwt": "eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9"
}
}
* Connection #0 to host 192.168.11.203 left intact
結果は200 OK
で、X-Verified-Jwt
内にアクセストークンが設定されてバックエンドサーバに渡っていることが返答からわかります。(httpbinはリクエストを返答のデータとして返すプログラムです。)
JWTのクレームベースルーティング
続いて、JWTのクレーム内容によってアクセス許可を設定します。
参考 JWT claim based routing追加で設定する箇所はVirtualService
で、クレームの内容を見てアクセスさせるか判断します。
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin
namespace: foo
spec:
hosts:
- "*"
gateways:
- httpbin-gateway
http:
- match:
- uri:
prefix: /headers
headers:
"@request.auth.claims.groups":
exact: group1
route:
- destination:
port:
number: 8000
host: httpbin
EOF
ここでは、groupsというクレームにgroup1が設定されている場合にのみ/headers
へアクセスを許可するというものです。
上記を設定して、先程使ったトークンをつかってアクセスを試みます。
$ curl -vvv --header "Authorization: Bearer $TOKEN" "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNnxWP2tcnR9q0vxyxweaF3ovQYHYZl82hAUsn21bwQd9zP7c-LS9qd_vpdLG4Tn1A15NxfCjp5f7QNBUo-KC9PJqYpgGbaXhaGx7bEdFWjcwv3nZzvc7M__ZpaCERdwU7igUmJqYGBYQ51vr2njU9ZimyKkfDe3axcyiBZde7G6dabliUosJvvKOPcKIWPccCgefSj_GNfwIip3-SsFdlR7BtbVUcqR-yv-XOxJ3Uc1MI0tz3uMiiZcyPV7sNCU4KRnemRIMHVOfuvHsU60_GhGbiSFzgPTAa9WTltbnarTbxudb_YEOx12JiwYToeX0DCPb43W1tzIBxgm8NxUg
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< date: Fri, 31 Dec 2021 15:24:38 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 192.168.11.203 left intact
404 Not Found
のエラーが返ってきています。トークンを確認するとgroupsのクレームがトークンに含まれていないことがわかります。
$ echo $TOKEN | cut -d '.' -f2 - | base64 --decode
{"exp":4685989700,"foo":"bar","iat":1532389700,"iss":"testing@secure.istio.io","sub":"testing@secure.istio.io"}
では、group1がgroupsに含まれたトークンを利用してアクセスしてみます。
TOKEN_GROUP=$(curl https://raw.githubusercontent.com/istio/istio/release-1.12/security/tools/jwt/samples/groups-scope.jwt -s)
curl -vvv --header "Authorization: Bearer $TOKEN_GROUP" "$INGRESS_HOST:$INGRESS_PORT/headers"
結果は以下のとおりです。
$ curl -vvv --header "Authorization: Bearer $TOKEN_GROUP" "$INGRESS_HOST:$INGRESS_PORT/headers"
* Trying 192.168.11.203:80...
* TCP_NODELAY set
* Connected to 192.168.11.203 (192.168.11.203) port 80 (#0)
> GET /headers HTTP/1.1
> Host: 192.168.11.203
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjM1MzczOTExMDQsImdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwiaWF0IjoxNTM3MzkxMTA0LCJpc3MiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsInNjb3BlIjpbInNjb3BlMSIsInNjb3BlMiJdLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.EdJnEZSH6X8hcyEii7c8H5lnhgjB5dwo07M5oheC8Xz8mOllyg--AHCFWHybM48reunF--oGaG6IXVngCEpVF0_P5DwsUoBgpPmK1JOaKN6_pe9sh0ZwTtdgK_RP01PuI7kUdbOTlkuUi2AO-qUyOm7Art2POzo36DLQlUXv8Ad7NBOqfQaKjE9ndaPWT7aexUsBHxmgiGbz1SyLH879f7uHYPbPKlpHU6P9S-DaKnGLaEchnoKnov7ajhrEhGXAQRukhDPKUHO9L30oPIr5IJllEQfHYtt6IZvlNUGeLUcif3wpry1R5tBXRicx2sXMQ7LyuDremDbcNy_iE76Upg
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Fri, 31 Dec 2021 15:31:07 GMT
< content-type: application/json
< content-length: 839
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 2
<
{
"headers": {
"Accept": "*/*",
"Host": "192.168.11.203",
"User-Agent": "curl/7.68.0",
"X-B3-Parentspanid": "a752f7a1accd9070",
"X-B3-Sampled": "0",
"X-B3-Spanid": "1aa1e4475e9cd886",
"X-B3-Traceid": "baffd2944c10614ea752f7a1accd9070",
"X-Envoy-Attempt-Count": "1",
"X-Envoy-External-Address": "100.96.1.1",
"X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=dc4427aa53abd305005efead810490c543a3757ae1f44529b918b5eea50c7558;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account",
"X-Verified-Jwt": "eyJleHAiOjM1MzczOTExMDQsImdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwiaWF0IjoxNTM3MzkxMTA0LCJpc3MiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyIsInNjb3BlIjpbInNjb3BlMSIsInNjb3BlMiJdLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9"
}
}
* Connection #0 to host 192.168.11.203 left intact
200 OK
と共にデータが返されています。また、トークンを確認するとgroup1がgroupsに入っていることがわかります。
$ echo $TOKEN_GROUP | cut -d '.' -f2 - | base64 --decode
{"exp":3537391104,"groups":["group1","group2"],"iat":1537391104,"iss":"testing@secure.istio.io","scope":["scope1","scope2"],"sub":"testing@secure.istio.io"}
最後に
Istioを利用してJWTの検証を外出しできることが確認できました。JWT自体がスタンダードなプロトコルなので、バックエンドサーバごとに実装する必要がありません。このように、API Gatewayなどへオフロードすることがマイクロサービスアーキテクチャの正しい実装となるでしょう。
ちなみに、Kongを使って同様のこともできそうなので時間があるときに試したいと思います。