😸

AWSアクセスキー管理を無くそう(Github Actions編)

2023/04/05に公開

Zenn 読者の皆さん、こんにちは。伊藤です。dotD で SRE をやっています。
はじめて記事を投稿してみます。

はじめに

みなさん、AWSは使っていますか?使ってますよね?
アクセスキーはどうでしょうか。Github Actions などで CICD を実装しているケースでアクセスキーを利用している方がいらっしゃるのではないでしょうか。

アクセスキーはAWSを手軽に利用するという点でとても便利なツールなのですが、メリットばかりでもありません。今回は Github Actions においてアクセスキーを使わずにCICD運用を実現した事例についてご紹介します。

アクセスキーを使う際のデメリット

アクセスキーを使う際のデメリットは、以下のようなあたりが挙げられるかなと思います。

  • サービスの情報漏えい
  • DDoS攻撃やマイニングに悪用される可能性がある
    • コンピュートリソース自体が価値になってきた時代なので、昨今最も事例が多い攻撃ではないかなと思います。
  • アクセスキー管理のための運用コストが掛かる
    • 不要なキーは削除したり、定期的にローテーションすべきものなので、それらの運用を継続するだけでコストがかかってしまいます。

アクセスキーを使わないことで100%これらのデメリットが回避できる、というわけではありませんが、アクセスキーに関しては使うメリットよりもデメリットの方が大きいと考えています。ということで、アクセスキーを使わない方法があるなら(生産性を多少犠牲にしてでも)基本的に私はそちらを推奨します。

ちなみに、セキュリティポリシの規定において一般によく利用されている CIS Benchmark でもアクセスキーの使用に関するプラクティスはいくつか規定されており、AWS Config で比較的簡単にポリシの適用ができるようになっています。例えばアクセスキーを90日でローテーションすること、などがあります。

AWS アクセスキーの利用を回避する in Github Actions

次に、AWSでアクセスキーを回避するための手順を紹介していきます。

理論編

GitHub Actions でアクセスキーの利用を回避するには、Web Identity Federationという機能を使って実現することができます。

Web Identity Federation では、SAML や OIDC(OpenID Connect) の技術を利用して、AWSの IAM Role にスイッチできるようにする、というものです。このサービス自体は re:Invent 2017 で発表されたもののようで、特に目新しいサービスというわけではありません。

SAML/OpenID Connect の仕組み

基本的にどちらの仕組みも、特定の IdP(Identity Provider) にログインしてから、発行されたトークンFを使ってSP(Service Provider) にアクセスするという仕組みの規格です。

以下の図ではとてもざっくりですが、IdP である Okta の認証を経て、そのトークンを使って Service Provider である Office365 にアクセスするという図になっています。

SAMLは XMLベース のプロトコルで、OIDC は JWT(JSON Web Token) を使用します。SAML よりも OpenID Connect の方が新しいプロトコルということもあり、リクエストサイズが小さかったり、実装も容易というメリットがあるようです。

Web Identity Federation のユースケース:モバイルアプリからのデータアクセス

AWS で紹介されているをご紹介します。モバイルアプリが直接 DynamoDB にアクセスするケースです。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/WIF.RunningYourApp.html

モバイルアプリ上に認証情報を格納することなく、ユーザがログインして取得した Amazon.com のトークンで DynamoDB へアクセスする、というこを実現しています。実際には、AWS STS に対して トークンを添えて特定のロールへの AssumeRole を要求し、STS がそのロールとして振る舞える一時的な認証情報をアプリケーションに返却します。

フロントエンドでビジネスロジックを完結するケース、ユーザのログを送るだけのようなビジネスロジックを挟む必要がないケースなどが考えられます。

実践編

私が運用している環境では、AWS SAM(Serverless Application Model) を利用してアプリケーションを動かしているので、今回はそれをサンプルとして動かしてみます。

構築は以下のような流れになります。公式ドキュメントはこちら

  1. AWS: OIDC Provider の登録
  2. AWS: スイッチ先ロールの修正
  3. Github Actions: AssumeRole 設定

1. AWS: OIDC Provider の登録

IAM > Access management から、Identity providers を登録します。この設定により、この IdP から発行されたトークンをAWS側で信頼するようになります。

https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

私が利用している環境では、以下のように設定しています。

Provider URL: https://token.actions.githubusercontent.com
Audience: https://github.com/[Org名]

Audience ですが、上記の公式ページでは sts.amazonaws.com となっていますが、任意のよりセキュアなパラメータに指定することができます。(以下)

The default audience is sts.amazonaws.com which you can replace by specifying the desired audience name in audience.

configure-aws-credentials

Org名は仮でa2-itoとしています。

※すでに同様のプロバイダが存在するので怒られていますが、、、

見てわかる通り、Github Actions の IdP を設定するのは複数登録できません。つまり、AWSアカウント1つにつき1 IdP です。。ただそのIdPに対して Add audience ができるようなので、複数組織のリポジトリの Github Actions を実行したい場合はそれでできるかもしれません。(試していません)

2. AWS: スイッチ先ロールの修正

次に、Github Actions の Runner がスイッチする先の IAM Role を作成していきます。

ここでのポイントは権限設定そのものよりも、Trust relationships (信頼関係) を適切に設定しているか、です。これが正しく設定されていないと、Runner が AssumeRole した時点でコケてしまいます。ここをきちんと設定できていることを確認してから、権限の精査をしていきましょう。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::[AWSアカウントID]:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:[Org名]/[Repo名]:*"
                }
            }
        }
    ]
}

なお、最終的に以下のようなIAMロールに設定しています。

IAMロール(長いので折りたたみにしました)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "cloudformation:CreateChangeSet",
            "Resource": "arn:aws:cloudformation:*:aws:transform/Serverless-2016-10-31",
            "Effect": "Allow",
            "Sid": "CloudFormationTemplate"
        },
        {
            "Action": [
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStacks",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:GetTemplateSummanry",
                "cloudformation:ListStackResources",
                "cloudformation:UpdateStack"
            ],
            "Resource": "arn:aws:cloudformation:*:[AWSアカウントID]:stack/*",
            "Effect": "Allow",
            "Sid": "CloudFormationStack"
        },
        {
            "Action": [
                "s3:CreateBucket",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::*/*",
            "Effect": "Allow",
            "Sid": "S3"
        },
        {
            "Action": [
                "lambda:AddPermission",
                "lambda:CreateFunction",
                "lambda:DeleteFunction",
                "lambda:GetFunction",
                "lambda:GetFunctionConfiguration",
                "lambda:ListTags",
                "lambda:RemovePermission",
                "lambda:TagResource",
                "lambda:UntagResource",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateFunctionConfiguration",
                "lambda:PublishVersion",
                "lambda:UpdateAlias",
                "lambda:CreateAlias",
                "lambda:DeleteAlias",
                "lambda:PutProvisionedConcurrencyConfig"
            ],
            "Resource": "arn:aws:lambda:*:[AWSアカウントID]:function:*",
            "Effect": "Allow",
            "Sid": "Lambda"
        },
        {
            "Action": [
                "iam:CreateRole",
                "iam:AttacheRolePolicy",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:GetRole",
                "iam:TagRole",
                "iam:PassRole"
            ],
            "Resource": "arn:aws:iam::[AWSアカウントID]:role/*",
            "Effect": "Allow",
            "Sid": "IAM"
        },
        {
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "lambda.amazon.com"
                }
            },
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Sid": "IAMPassRole"
        },
        {
            "Action": [
                "apigateway:DELETE",
                "apigateway:GET",
                "apigateway:PATCH",
                "apigateway:POST",
                "apigateway:PUT"
            ],
            "Resource": "arn:aws:apigateway:*::*",
            "Effect": "Allow",
            "Sid": "APIGateway"
        },
        {
            "Action": [
                "events:PutRule",
                "events:RemoveTargets",
                "events:PutTargets"
            ],
            "Resource": "arn:aws:events:*:[AWSアカウントID]:rule/*",
            "Effect": "Allow",
            "Sid": "CloudwatchEvents"
        }
    ]
}

公式のロール では、API Gateway に新規エンドポイントを追加したり、Cloudwatch Events を追加する場合などに権限が不足していたので、公式の内容にプラスアルファし、その結果として今の状態になっています。権限は Runner の中で何をどこまでやるかに依存しますので、各環境に応じてチューニングしてください。

3. Github Actions: AssumeRole 設定

あとは AssumeRole してデプロイを実行してあげればよいです。

シークレットとして設定する内容はAWS_ROLEとしてスイッチ先ロールを以下のように指定するだけです。

arn:aws:iam::[AWSアカウントID]:role/test-role

サンプルActions

  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
      role-to-assume: ${{ secrets.AWS_ROLE }}
      aws-region: ap-northeast-1
      role-duration-seconds: 1200
      role-session-name: github-actions
      audience: ${{ env.GITHUB_REPOSITORY_OWNER }}
  - name: Set up Python
    uses: actions/setup-python@v3
    with:
      python-version: "3.x"
  - name: Set up SAM
    uses: aws-actions/setup-sam@v2
  - run: aws sts get-caller-identity
  - name: Build SAM Application
    run: |
      sam build --use-container
  - name: Deploy SAM Application
    run: |
      sam deploy --config-env ${{ steps.set_output.outputs.DEPLOY_ENV }}

ハマったポイント

Github Actions のログからなぜその問題が起きているのか?が特定しづらい、というのがハマりポイントとなってしまう要因でした。Actions のエラーメッセージが原因を特定しづらいものだったので設定のどこかでミスっているということしかわからず、一つずつ設定をチェックしていく、という地道な作業をしていくしかありませんでした。(投稿時点でそのメッセージが残せていませんでした。すいません。)

その中で2点、やりがち(というか私がやってしまった)な設定ミスをご紹介します。

Trust Relationship のタイポ

AWS IAMロールに信頼関係として Github リポジトリ名を記載しますが、記載ミスがありました。以下のあたりです。

            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:[Org名]/[Repo名]:*"
                }
            }

OIDC のために必要な設定漏れ

Actions で OIDC を利用するための決り文句として以下の設定を付与する必要があります。

    permissions:
      id-token: write
      contents: read

注意点

投稿日(2023/4/3) 時点では、Github Actions である aws-actions/configure-aws-credentials が v2 になっています。パッと見大きな変化はなさそうですが、ここでは v1 前提の内容になっていますのでご注意ください。

その他 Web Identity Federation 活用例

今回紹介した AWS の Web Identity Federation ですが、私が実際に試したことがあるものも含め他のユースケースをご紹介します。

1. S3 -> Google Cloud 転送

私が過去のとある移行プロジェクトで実際にやっていた例です。結構多くのプロジェクトで使う機会があるのではないかと思いますが、S3上の大量データを GoogleCloud(Cloud Storage) に転送するというもので、Google Cloud 側では、CSTSというサービスを使うことでコードレスで実装することができます。感覚的にはAWS側のアクセスキーが必要そうな気がしますが、こちらも同様の仕組みでキーレスで実現できます。

公式ページに設定が記載されているので、ご参照ください。

2. 組織の個人アカウントでAWSリソースを管理

会社やプロジェクトにおいては、個人のアカウントは Google や AzureAD 等のIdPで管理されているケースが多いと思います。そのアカウントをAWSでもそのまま使ってしまおう、というユースケースです。

そうすることで、IAMアカウントを別に払い出したりする必要がなくなるので、普段業務に使っているアカウントでそのままAWSの管理ができます。

  • インフラチームはAWSリソースの管理をする
  • ビジネスチームはS3やデータベースからアクティビティログを参照する
  • 分析チームは Athena にクエリをなげる

といったところです。
AWSアカウント自体の管理が減るので、アカウント管理のコストが純減する可能性があると考えています。

一方で、実際問題としてはAWSを管理主体と組織のOAアカウントを管理主体は、多くの会社では違う組織と思います。そのため、一元管理することでOAアカウント管理主体の責任が相対的に重くなるという捉え方もあるので、そのあたりは難しい部分もあるのかなという気もしています。

まとめ

Web Identity Federation、非常に地味な機能ですが極めて強力な機能です。すでに色々な現場で利用されているのではないかと思いますが、セキュリティ、運用コストでメリットがあるので、まだ使っていない方はぜひ早めに導入することをおすすめします。

AWS以外の他のクラウドはというと、Github のこちらのページ をみると Azure や Google Cloud でもすでに同じようなサービスが提供されているようですね。Workload Identity Federation と呼ばれているようです。

SAML/OIDC といったオープンな技術がベースになっている仕組みなので、特定のクラウドに限らずまた特定のワークロードに限らず、有用な選択肢と思います。ただ次回はもっと派手なやつを紹介したいと思います。

dotDTechBlog

Discussion