EMD Blog

Github Actions에서 OIDC를 통해 AWS에 액세스 본문

Public Cloud/AWS

Github Actions에서 OIDC를 통해 AWS에 액세스

EmaDam 2023. 1. 5. 09:02

OIDC란 Open ID Connect의 약자로 일종의 인증 프로토콜이다. 이와 관련된 문서들을 보면 매커니즘이 OAuth2.0과 매우 비슷하다는 느낌을 받을 수 있는데 이는 OIDC가 OAuth2.0을 기반으로 하기 때문이다.

자세한 설명은 아래 문서 참고.

Github Actions에서 AWS에 접근하고자 할 때 AWS인증과 권한을 필요로 한다. 예를 들면 파이프라인에서 S3에 파일을 업로드 할 때나 ECR Private Repository Push 할 때가 그렇다.

이때 Github Actions에서는 다음처럼 인증을 수행할 수 있다. (repository)

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-2

위 방식은 AWS에서 적절한 권한이 포함된 IAM User를 생성한 뒤 액세스 키를 받아 Github Actions에 secret으로 등록하고 사용하는 방식이다.

물론 이렇게 직접 키를 등록해도 되겠지만 OIDC를 사용해 인증하는 방법을 사용할 수도 있다.

OIDC를 사용했을 때의 이점은 아래 문서를 통해 확인할 수 있다.

요약하면 다음과 같다.

  • 직접 키를 관리하지 않아 관리에 용이
  • AWS 리소스 접근제어를 보다 세부적으로 제어 가능
  • 단기 토큰을 발행하기 때문에 보안적으로 우수

OIDC를 통해 인증을 하기 위해서는 크게 3가지 작업이 필요하다.

  • AWS에 자격 증명 공급자에 OpenID Connect 타입으로 Github OIDC Provider를 등록
  • 신뢰정책을 생성 - 보안주체로 Github OIDC Provider 지정 및 Repository 필터링
  • 위 신뢰정책을 사용해 역할 생성 후 적절한 권한이 포함된 정책 지정

위 작업을 진행하면 Github Actions에서는 aws-actions/configure-aws-credentials를 사용해 단기 클라우드 액세스 토큰을 얻을 수 있다.

이 OIDC를 사용한 인증 과정은 다음과 같이 요약 가능하다.

  1. 정확하게는 Actions에서 Github OIDC Provider에 토큰을 요청하고 그 토큰을 받아 AWS에 전달
  2. AWS 신뢰정책을 통해 해당 토큰을 검증
  3. 검증 성공 시 단기 액세스 토큰을 발행

이때 토큰은 JWT으로 발행되며 안에 포함된 클레임은 아래 링크에서 상세하게 확인할 수 있다.

https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token

이제 동작 과정과 토큰 내용을 확인했으니 실제로 구성해보자.

먼저 AWS 자격 증명 공급자에 OpenID Connect 타입으로 Github OIDC Provider를 등록한다.

이때, 아래 문서들을 보면 발행자(iss)와 지문이 필요한 것을 알 수 있다.

https://docs.aws.amazon.com/cli/latest/reference/iam/create-open-id-connect-provider.html

iss의 아래 링크에 보면 iss를 확인할 수 있다.

https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token

https://token.actions.githubusercontent.com

하지만 지문의 경우 직접 확인을 해야하는데 이에 대한 내용은 아래 문서에서 확인할 수 있다.

https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html

문서를 보면 지문이란 것이 필요한 이유에 대해 설명해주고 있는데, 자격 증명 공급자를 생성할 때 등록하는 지문은 OIDC Provider가 사용하는 인증서의 인증기관에 대한 지문이며 이 지문을 통해 OIDC Provider의 인증서를 신뢰하게 된다.

Github OIDC Provider의 경우 인터넷에 쳐도 바로 나오긴한다. 하지만 최신화되어 있지 않을 수도 있기 때문에 아래 과정을 통해 얻는 것을 추천.

먼저 https://token.actions.githubusercontent.com/.well-known/openid-configuration에서 jwks_uri값을 찾는다.

$ curl https://token.actions.githubusercontent.com/.well-known/openid-configuration
{
	"issuer":"https://token.actions.githubusercontent.com",
	"jwks_uri":"https://token.actions.githubusercontent.com/.well-known/jwks",
	"subject_types_supported":["public","pairwise"],
	...
}

여기서 jwks_uri의 도메인만 복사해 아래처럼 인증서를 출력시킨다. (지금은 iss와 동일)

$ openssl s_client -servername token.actions.githubusercontent.com -showcerts -connect token.actions.githubusercontent.com:443
...
-----BEGIN CERTIFICATE-----
MIIG8jCCBdqgAwIBAgIQCn5zvdee2Vg6XXlzFLM1XDANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE
aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjExMDQwMDAwMDBa
Fw0yMzExMDcy
...
uJ5ZrAXfh48ZtVq/qmjfCX7f0ntUcsm85S2oNKAaKqqlGuwjA7ye80O3WHKQLXXM
evZ35QEWOlwhphLyHhUL6QFCuAe0wL2arESMXnxgaYE7Ka+SexxEiT5ZmdyrcFwg
BL7FKjOM
-----END CERTIFICATE-----
...
-----BEGIN CERTIFICATE-----
MIIE6jCCA9KgAwIBAgIQCjUI1VwpKwF9+K1lwA/35DANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
...
g0qZgYOrAKHKCjxMoiWJKiKnpPMzTFuMLhoClw+dj20tlQj7T9rxkTgl4ZxuYRiH
as6xuwAwapu3r9rxxZf+ingkquqTgLozZXq8oXfpf2kUCwA/d5KxTVtzhwoT0JzI
8ks5T1KESaZMkE4f97Q=
-----END CERTIFICATE-----
...

출력시키면 위처럼 2가지 인증서가 출력되는데 이 중 아래 인증서를 전부 복사해 certificate.crt라는 이름으로 저장한다.

(-----BEGIN CERTIFICATE----- , -----END CERTIFICATE-----포함)

그 다음 아래 명령어로 지문을 출력하면 된다.

$ openssl x509 -in certificate.crt -fingerprint -sha1 -noout | sed -e 's/://g' | tr '[:upper:]' '[:lower:]'
sha1 fingerprint=6938fd4d98bab03faadb97b34396831e3780aea1

이제 아래처럼 OIDC Provider를 등록할 수 있다.

$ aws iam create-open-id-connect-provider \\
--url <https://token.actions.githubusercontent.com> \\
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

자격증명 공급자를 등록했으니 이제 신뢰정책을 생성해야 한다.

신뢰정책을 구성하는 방법은 아래 문서에서 친절하게 안내해주고 있다.

https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#configuring-the-role-and-trust-policy

신뢰정책은 아래처럼 구성한다. 이 신뢰정책은 역할 생성시 json 형식으로 지정해주어야하기 때문에 json 파일로 생성한다.

$ cat > github-oidc-trust-policy.json << EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<aws_account_number>:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<user or organization>/<repository name>:*"
                },
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}
EOF

(<user or organization>/<repository name> 값은 Actions를 수행하는 자신의 repository 정보로 바꿔주자)

보안 주체(Principal)로 이전에 등록한 Github OIDC Provider ARN을 지정해주고 Actions으로 AssumRoleWithWebIdentity를 지정해주었다. AssumRole은 AWS 리소스에 액세스하기 위해 임시 자격증명을 얻는 작업인데, WithWebIdentity가 붙으면 웹 자격 증명 공급자를 통해 임시 보안 자격 증명을 얻을 수 있다.

그렇다면 위 신뢰정책은 보안주체인 Github OIDC Provider에 대해서 웹 자격 증명을 통한 AWS 리소스 액세스를 허용하겠다는 뜻인데, 저 보안 주체는 너무 광범위 하다.

이전에 GIthub OIDC Provider가 반환하는 토큰(JWT)을 통해 신뢰 관계를 검증한다고 했는데 이 JWT 클레임을 검증해서 신뢰할 수 있는 요청인지 확인할 수 있다.

검증은 위 신뢰정책에서 Condition(조건)에 해당하는 부분인데 내용을 보면 다음과 같다.

그리고 각 값을 살펴보면 다음과 같다.

sub: subject의 약자로 JWT에는 이 토큰을 전달한 repository 정보가 담겨있다.

aud: audience의 약자로 요청할 대상을 말한다. 이 문서를 보면 STS 요청은 https://sts.amazonaws.com이라는 단일 엔드포인트만 제공하고 있다. 왜 STS 요청이냐하면 AssumRoleWithWebIdentity가 STS의 API이기 때문.

위 값을 외에도 다른 클레임으로 조건을 작성할 수 있다. 클레임에 포함된 전체 내용은 아래 문서를 참고.

https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token

정리하면 위 신뢰정책의 내용은 다음과 같이 요약할 수 있다.

“이 정책은 보안주체인 Github OIDC Provider에 대해서 웹 자격 증명을 통한 AWS 리소스 액세스를 허용하지만 모든 요청에 대해 액세스를 허용하지 않고 repo:<user or organization>/<repository name> repository에서 sts.amazonaws.com로 요청한 건에 대해서만 허용해주겠다.”

이제 이 신뢰 정책으로 역할을 생성하자.

$ aws iam create-role \\
--role-name GithubOIDCRole \\
--assume-role-policy-document file://github-oidc-trust-policy.json

이제 임시보안자격증명을 얻을 수 있게 되었다. 하지만 이는 IAM 권한을 활용하기 위한 초기 설정일 뿐이다. 이 보안자격증명으로 필요한 AWS 리소스에 액세스 할 수 있도록 권한을 설정해야 한다.

권한 부여를 위해 정책을 하나 생성한다. 권한은 본인이 필요한 권한을 부여하면 된다. 지금은 ECR에 Push한다고 생각하고 아래처럼 권한을 지정했다. (ECR 이미지 Push에 대한 최소권한은 이 문서를 참고.)

cat > ecr-push-policy.json << EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:CompleteLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage"
            ],
            "Resource": "arn:aws:ecr:<region>:<aws_account_number>:repository/<repository_name>"
        },
        {
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        }
    ]
}
EOF
$ aws iam create-policy \\
--policy-name ECRPush \\
--policy-document file://ecr-push-policy.json

policy를 생성했으면 이전에 만든 GithubOIDCRole에 붙여주자.

$ aws iam attach-role-policy \\
--role-name GithubOIDCRole \\
--policy-arn arn:aws:iam::<aws_account_number>:policy/ECRPush

이제 역할이 준비되었으니 Github Actions에서 이 역할을 사용하도록 지정한다.

인증에 사용될 라이브러리(action)는 aws-actions/configure-aws-credentials이다.

제일 먼저 할 것은 Github OIDC Provider로 부터 JWT(AWS로 전달할 토큰)를 받을 수 있게 권한을 설정하는 것이다.

permissions:
  id-token: write
  contents: read

그 다음 AWS 인증을 통해 임시보안자격증명을 획득한다.

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    role-to-assume: arn:aws:iam::<aws_account_number>:role/GithubOIDCRole
    aws-region: ap-northeast-2

별다른 내용이 없는데 인증 자체를 role에 포함된 신뢰정책으로 하기 때문이다. role만 잘 지정해주자.

여기까지하면 AWS 인증은 끝이다. 만약 ECR Push도 하고 싶다면 아래 action을 사용하면 된다.

전체 샘플 코드는 다음과 같다.

name: "Continuous Integration - Spring App"
on:
  push:
    branches:
      - main

jobs:
  code-build:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: read
    needs: code-tests
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-java@v3
      with: 
        distribution: 'zulu'
        java-version: '8'
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: arn:aws:iam::<aws_account_number>:role/GithubOIDCRole
        aws-region: ap-northeast-2
    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
    - name: Build and push docker image to Amazon ECR
      env:
        REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        REPOSITORY: test-api
        IMAGE_TAG: ${{ github.sha }}
      run: |
        ./gradlew bootBuildImage --imageName=$REGISTRY/$REPOSITORY:$IMAGE_TAG
        docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest
        docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
        docker push $REGISTRY/$REPOSITORY:latest