Published on

GitHub Actions OIDC로 AWS 배포 권한 오류 해결

Authors

서버리스든 ECS든 EKS든, GitHub Actions에서 AWS로 배포할 때 가장 흔하게 마주치는 문제는 결국 AccessDenied 류의 권한 오류입니다. 예전처럼 장기 Access Key를 저장해 두면 당장은 해결되지만, 키 유출·로테이션·권한 과다 부여 문제가 따라옵니다.

OIDC(OpenID Connect) 기반 연동은 GitHub가 워크플로 실행 시점에만 짧게 쓸 수 있는 토큰을 발급하고, AWS가 그 토큰을 검증해 IAM Role을 AssumeRoleWithWebIdentity로 넘겨주는 방식이라 운영 관점에서 훨씬 안전합니다. 다만 설정이 조금만 어긋나도 곧바로 권한 오류가 발생합니다.

이 글은 “왜 권한 오류가 나는지”를 증상별로 분류하고, AWS IAM(신뢰 정책·권한 정책)과 GitHub Actions(permissions·audience·subject)의 정합성을 맞춰서 재발을 막는 데 초점을 둡니다.

관련해서 GitHub Actions 자체 디버깅 팁은 GitHub Actions 캐시가 안 먹을 때 디버깅 체크리스트도 함께 보면, 워크플로 진단 흐름을 잡는 데 도움이 됩니다.

OIDC 흐름 한 장 요약

  1. GitHub Actions Job이 id-token: write 권한을 갖고 OIDC 토큰을 요청합니다.
  2. aws-actions/configure-aws-credentials가 그 토큰을 AWS STS에 전달합니다.
  3. AWS STS는 IAM Role의 Trust Policy(신뢰 정책)를 검사합니다.
  4. 조건을 만족하면 STS가 임시 자격 증명(AccessKeyId/SecretAccessKey/SessionToken)을 발급합니다.
  5. 이후 AWS CLI/SDK는 임시 자격 증명으로 배포 API를 호출합니다.

문제는 3번(신뢰 정책)과 5번(권한 정책)에서 대부분 터집니다.

가장 흔한 에러 메시지와 원인 매핑

1) Not authorized to perform sts:AssumeRoleWithWebIdentity

  • IAM Role의 Trust Policy에 OIDC Provider가 없거나
  • sub/aud 조건이 불일치하거나
  • GitHub Actions에서 id-token: write를 주지 않아 토큰 자체가 발급되지 않는 경우

2) No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com

  • AWS 계정에 GitHub OIDC Provider 생성이 안 됨

3) AccessDenied: ... is not authorized to perform: ecr:PutImage (또는 ecs:RegisterTaskDefinition, cloudformation:CreateChangeSet 등)

  • Role은 Assume 되었지만, 배포에 필요한 IAM Permission Policy가 부족함

4) The security token included in the request is invalid

  • configure-aws-credentials 단계가 실패했는데도 다음 단계가 실행되었거나
  • region/환경변수 꼬임, 혹은 다른 크레덴셜이 덮어쓴 경우

이제부터는 “정상 작동하는 최소 구성”을 만들고, 거기에서 조건을 좁히는 방식으로 해결합니다.

1단계: AWS에 GitHub OIDC Provider 만들기

AWS 콘솔에서 IAM Identity providers에 OIDC Provider가 있어야 합니다.

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: 보통 sts.amazonaws.com

CLI로 만들면 대략 아래 형태입니다(인라인 코드 블록에만 https://를 쓰면 MDX 문제는 없습니다).

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

주의: thumbprint는 AWS 문서/콘솔 기준값을 확인하세요. 잘못 넣으면 이후 신뢰 검증이 실패합니다.

2단계: IAM Role Trust Policy(신뢰 정책) 제대로 만들기

OIDC 연동에서 가장 자주 틀리는 지점이 신뢰 정책의 Condition입니다.

Trust Policy 예시(레포 단위로 제한)

아래는 특정 레포의 main 브랜치에서 실행된 워크플로만 Role을 Assume할 수 있게 제한하는 예시입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

여기서 체크 포인트:

  • Federated ARN이 내 계정의 OIDC Provider ARN과 일치하는지
  • aud가 워크플로에서 요청하는 audience와 일치하는지
  • sub 패턴이 실제 실행 컨텍스트와 일치하는지

sub가 자주 어긋나는 케이스

  • PR에서 실행: ref:refs/pull/.../merge 형태가 될 수 있음
  • 태그 푸시: ref:refs/tags/v1.2.3
  • 환경 보호 규칙 사용: environment:prod 관련 claim을 추가로 제한하는 구성도 존재

브랜치/태그/PR을 모두 허용하고 싶다면 StringLike를 넓히되, 보안 경계를 어디에 둘지 먼저 정하세요.

예시(브랜치 main과 태그 v로 시작만 허용):

"StringLike": {
  "token.actions.githubusercontent.com:sub": [
    "repo:ORG/REPO:ref:refs/heads/main",
    "repo:ORG/REPO:ref:refs/tags/v*"
  ]
}

3단계: GitHub Actions 워크플로에 id-token: write 추가

OIDC 토큰 발급은 GitHub Actions의 권한 설정에 의해 막힐 수 있습니다. Job 또는 workflow 레벨에 아래가 필요합니다.

name: deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-2

      - name: Who am I
        run: aws sts get-caller-identity

aws sts get-caller-identity가 성공하면 “AssumeRole 단계”는 통과한 것입니다. 이제 남는 문제는 대부분 “배포 권한 정책 부족”입니다.

4단계: 배포 대상별 IAM Permission Policy 점검

OIDC는 인증(AssumeRole)까지의 문제를 해결해 주지만, 배포 API 권한은 별개입니다. 배포 방식별로 필요한 권한이 다릅니다.

ECR 푸시 권한(대표적인 ecr:PutImage 오류)

컨테이너 이미지를 푸시한다면 최소한 아래 권한들이 필요합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

리포지토리 단위로 더 강하게 제한하려면 Resource를 ECR repository ARN으로 좁히고, GetAuthorizationToken*로 남기는 식으로 분리합니다.

ECS 배포(자주 터지는 ecs:RegisterTaskDefinition, iam:PassRole)

ECS는 태스크 실행 역할을 넘겨줘야 해서 iam:PassRole이 빠지면 거의 반드시 실패합니다.

필요 권한 예시:

  • ecs:RegisterTaskDefinition
  • ecs:UpdateService
  • ecs:DescribeServices
  • iam:PassRole (태스크 실행 Role, 태스크 Role)

iam:PassRole은 반드시 대상 Role ARN으로 제한하세요.

S3 배포(정적 사이트)

  • s3:PutObject, s3:DeleteObject, s3:ListBucket
  • CloudFront 무효화까지 하면 cloudfront:CreateInvalidation

권한을 줄 때는 “버킷 ARN”과 “오브젝트 ARN”을 구분해야 합니다.

5단계: audience 불일치 문제 해결

어떤 설정/액션 조합에서는 audience를 명시해야 할 때가 있습니다. 기본은 sts.amazonaws.com이지만, 조직 정책이나 커스텀 구성이 들어가면 mismatch가 납니다.

configure-aws-credentials는 audience를 지정할 수 있습니다.

- name: Configure AWS credentials (OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
    aws-region: ap-northeast-2
    audience: sts.amazonaws.com

그리고 Trust Policy의 token.actions.githubusercontent.com:aud와 동일해야 합니다.

6단계: 디버깅 체크리스트(재현 가능한 순서)

권한 오류를 “감”으로 고치면 다음 배포에서 또 터집니다. 아래 순서로 한 단계씩 확정하세요.

  1. 워크플로에 permissions: id-token: write가 있는가
  2. configure-aws-credentials 단계 로그에서 OIDC 토큰 발급이 되었는가
  3. aws sts get-caller-identity가 성공하는가
  4. 실패한다면 Trust Policy의 Federated ARN, aud, sub를 재검증
  5. AssumeRole은 성공하지만 배포 API에서 실패한다면, 그 액션 이름 그대로 IAM 정책에 추가
  6. iam:PassRole이 필요한 서비스(ECS, CodeDeploy 등)인지 확인

이때 “어떤 액션이 거부되었는지”는 에러 메시지에 거의 항상 포함됩니다. 예를 들어 is not authorized to perform: cloudformation:CreateChangeSet처럼 액션이 보이면, 해당 액션을 최소 범위 리소스에 부여하면 됩니다.

권한/정책 문제를 진단하는 관점은 리눅스에서 권한 때문에 작업이 안 도는 상황을 점검하는 방식과 유사합니다. 체크리스트형 접근이 필요하다면 리눅스 logrotate가 안 돎? 권한·SELinux 점검도 같은 문제 해결 스타일로 참고할 만합니다.

7단계: 보안적으로 안전한 제한 패턴

OIDC의 장점은 “짧은 토큰”뿐 아니라 “정교한 조건 제한”입니다. 운영에서 권장하는 제한은 보통 아래 3가지입니다.

1) 레포 고정

  • subrepo:ORG/REPO:를 반드시 포함

2) 브랜치 또는 태그 고정

  • 배포는 main 또는 릴리즈 태그만

3) 환경 보호와 결합(선택)

  • GitHub Environments 승인(Protected environments)을 쓰는 경우, 환경 단위로 Role을 분리

예: prod 배포 Role은 main + 승인된 환경에서만 Assume 가능하게.

8단계: 자주 하는 실수 모음

  • Trust Policy에서 Actionsts:AssumeRole로 넣음 (OIDC는 sts:AssumeRoleWithWebIdentity)
  • sub를 너무 넓게 열어 repo:ORG/*로 허용 (편하지만 위험)
  • 워크플로 permissions를 생략해서 OIDC 토큰이 발급되지 않음
  • AssumeRole은 됐는데, ECR/ECS/S3 권한을 Role에 붙이지 않음
  • iam:PassRole 누락

마무리: 해결 전략은 “AssumeRole”과 “API 권한” 분리

GitHub Actions OIDC로 AWS 배포 권한 오류를 해결할 때 핵심은 문제를 두 층으로 분리하는 것입니다.

  • 1층: STS AssumeRole이 되는가 (신뢰 정책, aud, sub, id-token)
  • 2층: 배포 API 권한이 충분한가 (ECR/ECS/S3/CloudFormation 등)

먼저 aws sts get-caller-identity로 1층을 확정하고, 이후 에러 메시지에 나온 AWS API 액션을 기준으로 2층 권한을 최소 범위로 추가하면, 재현 가능하고 안전한 배포 파이프라인을 만들 수 있습니다.