GitHub Actionsに「強い」AWSの権限を渡したい ~作戦3 - AssumeRole with Google ID Token ~

こんにちは。技術部の池田です。

この記事では、Github Actions上に「強い」AWSの権限を渡すために以下のことを行います。

  • App Runnerでお手軽にGoogle ID Token 取得するためのWeb Applicationを動かす。
  • Web Applicationから取得できるGoogle ID Tokenを信頼するIAM RoleにAssumeRoleする。
  • AssumeRoleによって得られた一時的な強い権限で、強い権限を要求する作業(Deploy, Terraform Apply)をGithub Actionsで行う。

これにより、Github Actions上にAWSのアクセスキーを置かずに、ある程度安全な方法でAWS上での強い権限を要求する操作を実行できます。 そのため、例えばGithub Repositoryに不正アクセスされてしまったとしても、AWSの本番環境まで被害の及ぶ可能性が低くなります。

背景

5/14(金)に3社合同のSRE勉強会があり、弊社の藤原が『GitHub Actionsに「強い」AWSの権限を渡したい』という内容で発表いたしました。

speakerdeck.com

この発表の中では、以下の2つの作戦を取り上げていました。

  • 作戦1 - AssumeRole with MFA
  • 作戦2 - CludShell exports credentials

皆様も『GitHub Actionsに「強い」AWSの権限を渡したい』ようでして、様々な反応があったようです。
後日、藤原氏は新たな作戦を某所から仕入れてきたようです。
その新たな作戦について、概念実証を行いましたので記事にしたいと思います。

作戦3 - AssumeRole with Google ID Token

AWSのAssumeRoleにはいくつか種類があります。

  • AssumeRole
  • AssumeRoleWithSAML
  • AssumeRoleWithWebIdentity

よく使うのは、一番最初の普通のAssumeRoleだと思います。
しかしながら、 SAML 2.0-based Federation によるAssumeRole=AssumeRoleWithSAML OIDCのID Token等のWebIdentityによるAssumeRole=AssumeRoleWithWebIdentity も実は存在します。

以前の記事にて、Google Colabからお手軽にAmazon Athenaにアクセスを行いたくなり 、AssumeRoleWithWebIdentiyを用いました。

techblog.kayac.com

このように、Google ID TokenによるAssumeRoleし、一時的なAWS上での権限を取得できます。
Google ID Token は有効期限が1時間となっているため、有効期限が過ぎた後でもう一度権限を取得しようとしても失敗します。 前の 作戦1 - AssumeRole with MFAのコンセプトは、MFAトークンが約1分の有効期限であり、有効期限の短いものを使って権限を取得するというものでした。
作戦1も作戦3もコンセプトは同様であり、Google ID TokenもMFAトークンも、アクセスキーよりは外部に流出してしまったときの危険度は低いです。

さらに、作戦1 では、MFAデバイスを複数人で共有する必要があるというデメリットが発生していました。
例えば、開発チームのメンバー変更が起きたとき、場合によってはMFAデバイスを新しく設定し、新たなチームメンバーに共有するという作業が必要になります。

今回の作戦では各個人のGoogle ID Tokenを使用することができます。 権限を取得できるチームメンバーを変更したい場合は、IAM Roleの信頼関係を編集すれば良いだけとなります。 そのため、MFAトークンよりも有効期限が長い代わりに保守性が向上するというメリットがあります。

どうやってGoogle ID Tokenを手に入れよう?

今回はGoogle Colabなどのお手軽にGoogle ID Tokenを取得できる環境がありません。
(毎回 Colabを立ち上げてコードを書いてもらうわけにもいきません。)

何かお手軽に認証とかできる環境建てられないかなぁ〜〜〜 と考えていたところに
『App Runner』というサービスです! なんと、コンテナイメージをECRにPushするだけでWeb Applicationを動かせます!

aws.amazon.com

App Runnerを動かすインスタンスロールにSystems Manager パラメーターストア へのアクセスを与えておけば、 OAuth Client ID/Secret をコンテナに含めなくて良さそうです。
また、パラメーターストアに関しては弊社の長田が便利なOSS ssmwrapを作っておりますので取り扱いやすいです。
ということで、サクッとGo言語でWeb Application『google-jwt-viewer』を書いてみました。

github.com github.com

このgoogle-jwt-viewerの使い方は簡単です。

  1. パラメータストアに /apprunner/CLIENT_ID, /apprunner/CLIENT_SECRET を設定します
    • CLIENT_ID : Google OAuth2 のクライアントID
    • CLIENT_SECRET : Google OAuth2 のクライアントシークレット
  2. App Runner の環境変数に、SSMWRAP_PATHS=/apprunner/ を指定して google-jwt-viewerを起動します。
    • リポジトリのルートで以下のようにしてECRにPushするようのが楽だと思います。
$ docker build -t $(ECR_REPOSITORY):latest -f docker/Dockerfile .
$ docker push $(ECR_REPOSITORY):latest

f:id:ikeda-masashi:20210611180031p:plain

こんな感じの寂しい画面で、ID TokenであるJSON Web Token(JWT) を取得できるようになりました。
次は、このWeb Tokenを信頼できるIAM Roleを定義してみましょう。

IAM Roleの作成

IAM Role自体の作成で大事なポイントは信頼関係の定義のところです。 

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Federated": "accounts.google.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "accounts.google.com:aud": "<google-jwt-viewerで表示されているAudience>"
        },
        "ForAnyValue:StringEqualsIgnoreCase": {
          "accounts.google.com:email": [
            "***********@kayac.com",
            "<assume role できる人のemail address>"
          ]
        }
      }
    }
  ]
}

こんな感じです。
詳しくは以下のドキュメントを参照してください。 Google ID Token以外にもAmazon Cognito ID poolを用いた方法等もあります。 docs.amazonaws.cn

この信頼関係を持つIAM RoleでGithub Actionsに行わせたいことができる「強い」権限をつけます。 次は肝心のGithub ActionsのWorkflow定義です。

Github ActionsでID Tokenは取り扱い注意!

大まかには「作戦1 - AssumeRole with MFA」と同じようなWorkflowになるのですが、問題はID Tokenの取り扱いです。
MFAトークンのように有効期限がものすごく短いわけではないですし、一度きりしか使えないみたいな制約もありません。

LogからID Tokenが流出しないようにする。

有効期限が1時間とはいえ、有効期限が短いものの中では長い方です。 Github Actionsの実行Logで入力したID Tokenが見えてしまったら、有効期限が切れる1時間の間は使いたい放題です。 そこで、実行Log上では見えないようにするために、add-maskを利用します。

docs.github.com

echo "::add-mask::{value}" をワークフロー内で実行することで、それ以降に出力される値をマスクできます。 入力されたJWTに対してマスクを追加した例は以下のようになっています。

f:id:ikeda-masashi:20210614100827p:plain

AssumeRole自体はAWS CLIを使えば以下のようになります。

aws sts assume-role-with-web-identity \
    --role-arn ${{ secrets.IAM_ROLE_ARN }}  \
    --role-session-name github-actions-jwt \
    --duration-seconds 900 \
    --web-identity-token ${{ github.event.inputs.jwt }}

GithubのRepositoryのSecretsにIAM RoleのARNを設定しておけば自動的にマスクされるので、AssumeRole先もログ上では隠せます。

同じID Tokenによる再実行を防ぎたい。

作戦1のMFAトークンを使った場合は、副次的な効果でうっかり「Re-run jobs」での再実行を防ぐことが可能というメリットが有りました。
今回のID Tokenでは有効期限が長いので、30分後に別の誰かがそのID Tokenを使って再実行するということもできてしまいます。 再実行できないという特性を、作戦3でも行う必要があります。

再実行防止に関しては、Git Tagをつかうことで解決できました。 ID Tokenに由来したGit Tagを生成し、Github ActionsのWorkflow内でtagを設定します。 Workflowの最後でそのタグをRemoteにPushします。

一度終わった実行に関して「Re-run jobs」と雑に押しても、すでにTagが存在していますので、Workflowが失敗します。 DeployやTerraform Apply等を行っているのであれば、自動的にTagが設定されるので、いつ、どのCommit時点で反映したのかもわかりやすいです。

今回は、ID Token のペイロードにnonceがあるので、それを使うことにしました。
ID Tokenごとに同じであれば良いので、単純にSHAハッシュを取るのでも良いとは思います。

Workflow 全貌

最終的なWorkflowは以下のようになります。この例はTerraform Applyすることを想定しています。

name: terraform-apply-with-jwt
on:
  workflow_dispatch:
    inputs:
      id_token:
        description: 'JWT'
        required: true

jobs:
  manual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: set Git Terrform Apply Tag
        run: |
          set -e
          echo "::add-mask::${{ github.event.inputs.id_token }}"
          ID_TOKEN=${{ github.event.inputs.id_token }}
          PART=(${ID_TOKEN//./ })
          PAYLOAD=${PART[1]}
          PAYLOAD_LEN=$((${#PAYLOAD} % 4))
          if [ $PAYLOAD_LEN -eq 2 ]; then
            PAYLOAD="$PAYLOAD"'=='
          elif [ $PAYLOAD_LEN -eq 3 ]; then
            PAYLOAD="$PAYLOAD"'='
          fi
          NONCE=$( echo $PAYLOAD | base64 -d | jq -r '.nonce')
          EMAIL=$( echo $PAYLOAD | base64 -d | jq -r '.email')
          git config --local user.email $EMAIL
          TFAPPLY_GIT_TAG=terraform-apply_$NONCE
          git pull origin --tags
          git tag $TFAPPLY_GIT_TAG
          echo TFAPPLY_GIT_TAG=$TFAPPLY_GIT_TAG >> $GITHUB_ENV
          echo "terraform apply by $EMAIL"
        shell: bash
      - name: Assume Role With Web Identity
        run: |
          set -eu
          OUTPUT=$(aws sts assume-role-with-web-identity \
            --role-arn ${{ secrets.IAM_ROLE_ARN }}  \
            --role-session-name github-actions-jwt \
            --duration-seconds 900 \
            --web-identity-token ${{ github.event.inputs.jwt }} \
          )
          AWS_ACCESS_KEY_ID=$(echo $OUTPUT | jq -r '.Credentials.AccessKeyId')
          echo "::add-mask::$AWS_ACCESS_KEY_ID"
          echo AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID >> $GITHUB_ENV
          AWS_SECRET_ACCESS_KEY=$(echo $OUTPUT | jq -r '.Credentials.SecretAccessKey')
          echo "::add-mask::$AWS_SECRET_ACCESS_KEY"
          echo AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY >> $GITHUB_ENV
          AWS_SESSION_TOKEN=$(echo $OUTPUT | jq -r '.Credentials.SessionToken')
          echo "::add-mask::$AWS_SESSION_TOKEN"
          echo AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN >> $GITHUB_ENV
        shell: bash
        env:
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_EC2_METADATA_DISABLED: true
      - name: terraform apply
        run: |
          make terraform/apply
        env:
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_EC2_METADATA_DISABLED: true
      - name: Git Terrform Apply Tag Push
        run: |
          git push origin $TFAPPLY_GIT_TAG
        shell: bash

これまでの、作戦と比較して「いいところ」と「わるいところ」は以下のようになります。

  • いいところ:
    • 作戦1 - AssumeRole with MFAと違ってタイムアタック要素がない。
    • MFAの設定等の複数人で共有するものはない。
    • 作戦2 - CludShell exports credentialsと違い関係ない外部のサービスにアクセスキーなどが通過しない。
    • そもそも、Repositoryにアクセスキーを設定する必要がない。
  • わるいところ:
    • Google ID Tokenの有効期限が1時間と少し長い(ユースケースに対して)

Google ID Tokenの有効期限が長めであることに対しては、もっと短い5分の有効期限のID Tokenを発行できるAWS Cognitoを使うという手もあります。 しかし、AWS CognitoのID Pool周りの設定が少しややこしいので、お手軽さではGoogle ID Tokenに軍配が上がります。 AWS Cognitoを使ってAssumeRoleWithWebIdentityをする場合は、クラスメソッド株式会社様の記事を参考に実装していただければと思います。 dev.classmethod.jp

おわりに

この記事では、Github Actions上に「強い」AWSの権限を渡すために以下のことを行いました。

  • App Runnerでお手軽にGoogle ID Token 取得するためのWeb Applicationを動かす。
  • Web Applicationから取得できるGoogle ID Tokenを信頼するIAM RoleにAssumeRoleする。
  • AssumeRoleによって得られた一時的な強い権限で、強い権限を要求する作業(Deploy, Terraform Apply)をGithub Actionsで行う

CI/CD周りの悩み解決の一助になれば幸いです。

カヤックではCI/CDで強い権限を取り扱えるエンジニアも募集しています

中途採用も募集しています

2021/06/15 追記

記事公開の後日、弊社の藤原宛にお返事がありました。

なんと!もっとスッキリできるとのことです。 また、その他にも多重起動対策を加えられたため、改良版を追加で記載します。 改良点としては以下の3点です。

  • AssumeRoleWithWebIdentityをAWS SDKにおまかせすることで、記述がスッキリ
  • Git Tagの生成を単純にJWTのhashにすることで、記述がスッキリ
  • 慌てて急いでボタンを連打してしまい、多重起動になってしまっても失敗になるように!
name: terraform-apply-with-jwt
on:
  workflow_dispatch:
    inputs:
      id_token:
        description: 'JWT'
        required: true

jobs:
  manual:
    runs-on: ubuntu-latest
    env:
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_EC2_METADATA_DISABLED: true
    steps:
      - uses: actions/checkout@v2
      - name: set Git Terrform Apply Tag
        run: |
          echo "::add-mask::${{ github.event.inputs.jwt }}"
          TFAPPLY_GIT_TAG=deploy-$(echo -n {{ github.event.inputs.jwt }} | shasum | cut -d " " -f1)
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git tag $TFAPPLY_GIT_TAG
          git push origin $TFAPPLY_GIT_TAG
        shell: bash
      - name: Assume Role With Web Identity
        run: |
          set -eu
          TEMP_DIR=$(mktemp -d)
          echo ${{ github.event.inputs.jwt }} > $TEMP_DIR/jwt
          echo AWS_ROLE_ARN=${{ secrets.IAM_ROLE_ARN }}  >> $GITHUB_ENV
          echo AWS_WEB_IDENTITY_TOKEN_FILE=$TEMP_DIR/jwt >> $GITHUB_ENV
          echo AWS_ROLE_SESSION_NAME=github-actions-jwt >> $GITHUB_ENV
        shell: bash
      - name: terraform apply
        run: |
          make terraform/apply
        env:
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_EC2_METADATA_DISABLED: true

とてもコンパクトに仕上がりました。 アドバイスいただきありがとうございました。

カヤック×primeNumber×クラシコム合同SRE勉強会を開催しました

カヤックSREチームの今です。

5/14(金)に3社合同のSRE勉強会をオンライン開催しました。
参加企業は、カヤック、クラシコム様、primeNumber様です。

SREはまだまだ一般的ではなく、知見の少ない役職です。また企業内での人数も少ないこともあり、普段同じ技術領域について話す人があまりいません。
そこで今回のSRE勉強会は、企業の垣根を越えた知見の共有と同役職者同士の親睦を深めよう、という趣旨で開催されました。

その発表資料と一部を抜粋してご紹介します。

GitHub Actionsに「強い」AWSの権限を渡したい (カヤック 藤原

speakerdeck.com

terraform applyなどの強い権限を必要とする操作をGitHub Actions等で継続的に実行するためには強い権限をもたせる必要がありますが、セキュリティ上の懸念点も増えることになります。 そこで、強い権限を一時的に渡すため考えた2通りのアプローチを紹介しました。

デプロイ頻度を高めるための自動化と、権限の強いアカウントのクレデンシャルの保護はサービスの信頼に繋がるものなので、悩みの種なのは各社同じなようでした。
GitHub Actionsは便利になる機能が続々リリースされているので、これからの新機能の動向も気になります。

trocco SREチームの概要と取り組みについて(primeNumber 百々さん

speakerdeck.com

primeNumberで運用しているデータ統合自動化SaaS「trocco」では数多くのデータソースをサポートしており、その特徴から障害の原因も多岐にわたります。 その中、安定して稼働させるためにSREチームが行っている内容を紹介していただきました。

モニタリングやダッシュボードの強化から始まり、エンジニアリングチームを越えてCSチームや営業チームまで幅広く関わって安定稼働のための活動をしているとのことでした。 チームを横断しての活動は座組を組むことが難しいことが多いのですが、しっかり連携して安定性・ユーザーの満足度の向上のための活動が出来ているのが流石でした。

SLI/SLOに関する雑多な悩み(カヤック 池田

f:id:tkonsoy:20210525174729p:plain
(スライド非公開)

SREチームの活動指標となるSLI/SLOの設定について、カヤックで運営している大会運営サービス「Tonamel」で起きた困り事について紹介しました。
本当にユーザーから見える可用性は計測が難しいのでサーバーのメトリクスをSLIとして利用しますが、サービスの種類によってはメトリクスから算出するものが困難であったり、例としてGraphQLはエラー時に200を返すのでエラーレートをSLIとして扱うのが難しい、などの問題に直面しました。
SLI/SLO設定の難しさとは長い付き合いになりそうです、積極的に会社を越えて知見を共有していきたい話題でした。

北欧、暮らしの道具店のAWSマルチアカウント運用(クラシコム 佐々木さん)

speakerdeck.com

開発の"歴史"により1VPCに本番・ステージング・テスト環境が混在したり、アカウントに過剰な権限が付与されている状態から整理して、マルチアカウントで適切な権限、環境の小分けをしてわかった利点、また苦労したことについて発表していただきました。
発祥不明なリソースが本番環境のすぐ側にあった、などの経験は多くのインフラエンジニアが体験していると思います。確認の手間が増えたり、事故の原因にもなるので恐ろしいですよね。

細かくなったIAMロールが増えて管理が大変になる問題には、AWSコンソールではWeb拡張のAWS Extend Switch Roles、aws-cliにはaws-vaultを利用することで管理を楽にできるとのことです。
aws-vaultは今回初めて知ったのですが、シークレットを.aws/credentialsに平文で書かずOSのkeychainに保存できるのが気持ち的に楽なのでいいなぁとなりました。


発表後には懇親会も開催されました。
おすすめのお酒の話から、発表内容についてもう少し深堀りした内容を話したり、最近行われた弊社の新卒研修についての話をしてみたりした気がします(記憶がない)。
SRE Nextなどの大きいイベントはありましたが、SREとして集まって少人数でより深く話せる会は貴重な時間でした。

f:id:tkonsoy:20210525174831p:plain
記念写真(スクショ)

AWSアカウントの管理、SLI/SLOの設定についてのあるある話に花を咲かせた初回でした。
また次回の勉強会も企画中なので、また面白い話(あるいはつらかった話)が聞けるのを楽しみにしています。

カヤックでは合同勉強会を開催していただける会社さんを募集しております。興味がありましたら、カヤックお問い合わせフォームまで是非ご連絡ください。
まだまだ業界を通して知見が浅いSREという役職ですが、お互いのサービスをより良くするための情報共有ができると嬉しいです。

また、一緒にお仕事をする仲間も随時募集しています!
https://www.kayac.com/recruit/career

以上、カヤックSREチームの今でした。