GKEのPodからTokenRequestProjectionで発行されたOIDCのID Tokenを使用してAWSのRoleにAssumeRoleしてみる

AvatarPosted by

この記事は GRIPHONE Advent Calendar 2021 2日目の記事です。

SREの徳田です。

GKEのTokenRequestで発行されたID Tokenって外から検証できるんだっけ・・?という疑問から色々探していたところ、OpenID Connect Discoveryまで実装されており外部からID Tokenの検証が可能だったため、これを活用して今回はAWSのRoleにAssumeRoleしてみようと思います。

TL;DR

  • TokenRequestProjectionでTokenをマウント
  • AWSでOIDC Identity Providerの設定とそこからAssumeRoleできるIAM Roleの作成
    • Provider URLには https://container.googleapis.com/v1/projects/{{ PROJECT_ID }}/locations/{{ LOCATION }}/clusters/{{ CLUSTER_NAME}} を指定
  • 環境変数を適宜設定し、ID TokenにAudienceを設定したPodを作成
    • AWS_ROLE_ARN
    • AWS_WEB_IDENTITY_TOKEN_FILE
    • AWS_ROLE_SESSION_NAME

TokenRequestProjectionを使ってID Tokenをマウント

いきなりですが、TokenRequestProjectionを使うとshort-livedなTokenを動的に生成し、Projected Volumeを使ってPodのコンテナにマウントすることができます。

apiVersion: v1
kind: Pod
metadata:
  name: token-request-projection
spec:
  containers:
  - name: main
    image: alpine
    command:
    - ash
    - -c
    - |
      apk add --no-cache curl > /dev/null 2>&1
      curl -sk https://kubernetes/api
      echo
      curl -sk -H "Authorization: Bearer `cat /secret/token`" https://kubernetes/api
    volumeMounts:
    - name: token
      mountPath: /secret
      readOnly: true
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          path: token
  restartPolicy: Never

このとき生成されるTokenはOIDC準拠のID Tokenです。これをkube-apiserverに投げてAPIを呼び出すことができます。上記のManifestを適用してログを見てみると、呼び出せてることがわかると思います。

$ kubectl apply -f token-request-projection.yaml
pod/token-request-projection created
$ kubectl logs token-request-projection
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/api\"",
  "reason": "Forbidden",
  "details": {

  },
  "code": 403
}
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "34.84.124.238:443"
    }
  ]
}

さて、このToken、OIDC準拠のID Tokenということで、Issuerはどうなっているのでしょうか?確認してみましょう。以下のManifestを作成します。

apiVersion: v1
kind: Pod
metadata:
  name: decode-idtoken
spec:
  containers:
  - name: main
    image: alpine
    command:
    - ash
    - -c
    - |
      cat /secret/token | \
      cut -d. -f2 | \
      base64 -d
    volumeMounts:
    - name: token
      mountPath: /secret
      readOnly: true
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          path: token
  restartPolicy: Never

適用してログを見てみましょう。

$ kubectl apply -f decode-idtoken.yaml
pod/decode-idtoken created
$ kubectl logs decode-idtoken
{"aud":["https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi"],"exp":1638210931,"iat":1638207331,"iss":"https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi","kubernetes.io":{"namespace":"default","pod":{"name":"decode-idtoken","uid":"95e5a286-1037-4f30-9c7d-5e123f0a5d3d"},"serviceaccount":{"name":"default","uid":"67e14e43-91fb-4e1e-9ebb-9a927abbb2bd"}},"nbf":1638207331,"sub":"system:serviceaccount:default:default"}

見づらいですね、整形したものがこちら。

{
  "aud": [
    "https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi"
  ],
  "exp": 1638210931,
  "iat": 1638207331,
  "iss": "https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "decode-idtoken",
      "uid": "95e5a286-1037-4f30-9c7d-5e123f0a5d3d"
    },
    "serviceaccount": {
      "name": "default",
      "uid": "67e14e43-91fb-4e1e-9ebb-9a927abbb2bd"
    }
  },
  "nbf": 1638207331,
  "sub": "system:serviceaccount:default:default"
}

ID Tokenらしき情報が色々ありますが、iss に注目してみましょう。

https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi

というこちらのリンクがこのID TokenのIssuerです。で、このIssuerがOpenID Connect DiscoveryをサポートしていればIssuerに関する情報がでてくるはず・・・。

おもむろにcurlを投げてみます。

$ curl https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi/.well-known/openid-configuration
{
  "issuer": "https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi",
  "jwks_uri": "https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi/jwks",
  "response_types_supported": [
    "id_token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "kubernetes.io"
  ],
  "grant_types": [
    "urn:kubernetes:grant_type:programmatic_authorization"
  ]
}

さて、OpenID Connect Discoveryがサポートされていたようで、Issuerに関する情報がでてきました。 jwks_uri もあります。

ということは、最近流行りのOIDCのID Tokenを使った認証ができるのでは!?ということでGKEからAWSの認証ができるように設定してみます。

AWSにOpenID Connect Identity Providerを設定

まず、先に jwks_uri の証明書のCAのthumbprintを出しておきます。こちらを参考に出します。

jwks_uri の証明書を表示させましょう。

$ openssl s_client -showcerts -connect container.googleapis.com:443
CONNECTED(00000005)
depth=3 C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA
verify return:1
depth=2 C = US, O = Google Trust Services LLC, CN = GTS Root R1
verify return:1
depth=1 C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
verify return:1
depth=0 CN = *.googleapis.com
verify return:1
---
Certificate chain
 0 s:/CN=*.googleapis.com
   i:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
-----BEGIN CERTIFICATE-----
MIIa8TCCGdmgAwIBAgIRAOoWrQcm7qqTCgAAAAEZUxEwDQYJKoZIhvcNAQELBQAw
〜〜〜中略〜〜〜
f54xPmxU//OmW5wlNkF9Cx/Cei4HU20cObW3rqxsN5GAOxCkOw==
-----END CERTIFICATE-----
 1 s:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
   i:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw
〜〜〜中略〜〜〜
1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd
-----END CERTIFICATE-----
 2 s:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
   i:/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
-----BEGIN CERTIFICATE-----
MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX
MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE
CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx
OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT
GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63
ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS
iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k
KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ
DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk
j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5
cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW
CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499
iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei
Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap
sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b
9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP
BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf
BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw
JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH
MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al
oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy
MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF
AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9
NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9
WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw
9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy
+qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi
d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=
-----END CERTIFICATE-----
---
Server certificate
subject=/CN=*.googleapis.com
issuer=/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
---
No client certificate CA names sent
Server Temp Key: ECDH, X25519, 253 bits
---
SSL handshake has read 10395 bytes and written 281 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-CHACHA20-POLY1305
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-CHACHA20-POLY1305
    Session-ID: 3974D98852FC6912236D830CDCAD6E3753002A63C1DEA136EB532930D69B5B49
    Session-ID-ctx:
    Master-Key: 2A722D19954EBFF022D1C064CCCAC6A746920544071D69F0F4EECD9F573DE81D2708E0B5336DAD08EEA3E817D169012B
    TLS session ticket lifetime hint: 100800 (seconds)
    TLS session ticket:
    0000 - 01 10 60 ee 4e ac ad 1e-37 9d 1c 71 fc 29 2b a7   ..`.N...7..q.)+.
    0010 - 31 d3 1f 64 03 5f 38 10-60 33 87 b9 7b 06 0d 2c   1..d._8.`3..{..,
    0020 - 3f 89 9b fd 35 78 b5 84-d3 b3 1e 02 2a 25 3c 60   ?...5x......*%<`
    0030 - 05 2a 21 67 81 31 e2 a1-81 b3 d6 bb c9 9e 9e b5   .*!g.1..........
    0040 - 2e db d9 b9 bb 0e 9a 45-72 83 af d3 80 a1 4a 54   .......Er.....JT
    0050 - 0b d8 fa d7 14 15 dc 59-7a 2d 62 c8 cb ad e4 55   .......Yz-b....U
    0060 - 84 f6 10 04 a3 9e 93 15-70 3f a6 d6 14 7f ff 11   ........p?......
    0070 - b4 38 5e d1 7c c1 a0 64-23 cf 04 01 2f 2c b1 9a   .8^.|..d#.../,..
    0080 - 2a dc 37 aa 30 57 37 a5-81 ee da bf 78 0e d5 77   *.7.0W7.....x..w
    0090 - 2b 08 b4 9f cc 65 5b fa-c2 01 4a 78 4b 48 92 af   +....e[...JxKH..
    00a0 - 4a 1c 58 46 0d f0 f6 16-55 ee e1 48 c6 4f de ac   J.XF....U..H.O..
    00b0 - 70 d6 b5 52 91 9a 81 48-31 33 d1 af d6 bc cf 4e   p..R...H13.....N
    00c0 - 72 b0 8a 74 ca e8 a7 26-6c 72 16 d0 c7 1f 65 0a   r..t...&lr....e.
    00d0 - 5d 09 8e 98 28 3e 15 72-70 e5 28 d9 f7            ]...(>.rp.(..

    Start Time: 1638209772
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---
^C

表示された最後の証明書を保存しておき(今回は cert.pem とした)、以下のコマンドでthumbprintを出します。

$ openssl x509 -in cert.pem -fingerprint -noout | cut -d= -f2 | tr -d 08745487E891C19E3078C1F2A07E452950EF36F6

それではOpenID Connect Identity Providerを作成します。

--url にはIssuerのURL、 --client-id-list にはID TokenのAudience、 --thumbprint-list には先程出した値を入れます。

$ aws iam create-open-id-connect-provider \
--url "https://container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi" \
--client-id-list aws \
--thumbprint-list 08745487E891C19E3078C1F2A07E452950EF36F6
arn:aws:iam::152732720536:oidc-provider/container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi

委任するIAM Roleの作成

先程作成したIdentity ProviderからAssumeRoleできるIAM Roleを作成しましょう。

まず以下のようなPolicyファイルを作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::152732720536:oidc-provider/container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi"
      },
      "Action": "sts:AssumeRoleWithWebIdentity"
    }
  ]
}

Federated のところに先程作成したIdentity ProviderのARNを入力します。

そしてIAM Roleを作成します。今回特に権限付与は行いません。

$ aws iam create-role --role-name gke-pod --assume-role-policy-document file://policy.json
ROLE    arn:aws:iam::152732720536:role/gke-pod  2021-11-29T18:40:44+00:00       /       AROASHD4ULWMEWXJQ57F3   gke-pod
ASSUMEROLEPOLICYDOCUMENT        2012-10-17
STATEMENT       sts:AssumeRoleWithWebIdentity   Allow
PRINCIPAL       arn:aws:iam::152732720536:oidc-provider/container.googleapis.com/v1/projects/teak-catwalk-333611/locations/asia-northeast1-a/clusters/test-wi

GKEのPodからAssumeRoleしてみる

それでは最後にGKEのPodからAssumeRoleしてみましょう。

以下のようなManifestを作成します。

apiVersion: v1
kind: Pod
metadata:
  name: assume-role-from-gke
spec:
  containers:
  - name: main
    image: amazon/aws-cli
    command:
    - aws
    - sts
    - get-caller-identity
    - --output
    - text
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::152732720536:role/gke-pod
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /secret/token
    - name: AWS_ROLE_SESSION_NAME
      value: assume-role-from-gke
    volumeMounts:
    - name: token
      mountPath: /secret
      readOnly: true
  volumes:
  - name: token
    projected:
      sources:
      - serviceAccountToken:
          audience: aws
          path: token
  restartPolicy: Never

要点としては、3つの環境変数と audience を設定することです。

  • AWS_ROLE_ARN には委任するIAM Roleで作成したRoleのARNを指定
  • AWS_WEB_IDENTITY_TOKEN_FILE にはID Tokenのファイルパスを指定
  • AWS_ROLE_SESSION_NAME にはそのロールセッションの名前を指定。わかり易い名前だったら何でもいいかと。
  • audience にはOpenID Connect Identity Providerを作成した際に --client-id-list で指定した値を入れる

それではこのManifestを適用してログを見てみましょう。

$ kubectl apply -f assume-role-from-gke.yaml
pod/assume-role-from-gke created
$ kubectl logs assume-role-from-gke
152732720536    arn:aws:sts::152732720536:assumed-role/gke-pod/assume-role-from-gke     AROASHD4ULWMEWXJQ57F3:assume-role-from-gke

こんな感じでTokenRequestProjectionで発行されたID Tokenを使ってAWSのRoleにAssumeRoleすることができました🎉

ついでに

GKEのOpenID Connect Discoveryのドキュメントが見つからないな〜と思ったら、API Referenceにはありました。

https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.locations.clusters.well-known/getOpenid-configuration

This API is not yet intended for general use, and is not available for all clusters.

v1のAPIなのに・・・ふむ・・・🤔

という感じで紹介しておいてあれですが、使うにあたっては自己責任で😅

終わりに

GKEのクラスタからTokenRequestProjectionで発行されたのID Tokenを使ってAWSのRoleにAssumeRoleできました。

OIDCのID TokenなのでAWSに限らず色々なところで使えそうですね。

GCP内でもhostNetworkが有効になっているPodなどにも設定すると良さそうかな〜と思っています。

それでは!!