JWTの検証をIstioに外出しする

Go言語を使って、JWTを検証するコードを実装した記事の続きです。

[Golang] OIDCのトークンを検証する

JWTの検証をバックエンドサーバで実装すると、冗長的な構成となるため、外出しできるならばすべきコンポーネントです。そこで今回は、サービスメッシュで知られているIstioを使って、JWT検証を外出ししたいと思います。

本記事はこんな人におすすめ!
  • JWTの検証をIstioで行いたい
  • Kuberentes環境でJWTの検証を考えている

さっそく動かしてみましょう。

概要

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を使って同様のこともできそうなので時間があるときに試したいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA