Published on

GitHub Actions Docker CI/CD 권한(permis…) 오류 해결

Authors

서론부터 결론까지 한 번에 읽고 바로 고칠 수 있도록, GitHub Actions에서 Docker 기반 CI/CD를 만들 때 반복적으로 발생하는 permission denied 류 문제를 유형별로 정리합니다. 에러 메시지는 비슷해 보여도 실제 원인은 (1) GitHub 토큰 권한, (2) 레지스트리 인증/스코프, (3) 러너의 Docker 데몬 접근, (4) 파일/워크스페이스 권한, (5) 캐시/아티팩트 권한으로 갈립니다. 특히 “permis…”로 잘려 보이는 로그(예: permission denied, denied: requested access, insufficient_scope)는 초반에 정확히 분기하지 않으면 삽질 시간이 길어집니다.

아래는 가장 흔한 실패 시나리오를 에러 문구 → 원인 → 해결로 매핑하고, 바로 붙여 넣어 쓸 수 있는 워크플로 예제까지 포함한 가이드입니다.

1) 먼저 로그에서 "어떤 권한"인지 분기하기

권한 오류는 크게 두 종류입니다.

1.1 GitHub/레지스트리 인증·권한(HTTP 401/403 계열)

  • 예시
    • denied: requested access to the resource is denied
    • unauthorized: authentication required
    • insufficient_scope: authorization failed
    • 403 Forbidden (레지스트리 API)

이 경우는 대체로 토큰 스코프 또는 레지스트리 권한 문제입니다.

1.2 러너 내부 파일·소켓 권한(Unix permission denied)

  • 예시
    • permission denied while trying to connect to the Docker daemon socket
    • Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
    • EACCES: permission denied, open ...

이 경우는 Docker 데몬 접근 권한 또는 워크스페이스 파일 소유자/권한 문제입니다.

2) GHCR push가 실패: GITHUB_TOKEN 권한 부족

가장 흔한 케이스입니다. GitHub Container Registry(GHCR)에 push할 때 GITHUB_TOKEN이 기본 권한으로는 부족하거나, 리포지토리/조직 정책 때문에 막힙니다.

2.1 전형적인 증상

  • docker push ghcr.io/<owner>/<image>:<tag>에서 denied: requested access to the resource is denied

2.2 해결: workflow에 permissions 명시

GITHUB_TOKENpackages: write가 필요합니다. 또한 private repo/조직 정책에 따라 contents: read도 명시하는 편이 안전합니다.

name: ci
on:
  push:
    branches: [ main ]

permissions:
  contents: read
  packages: write

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }}

2.3 조직(Org) 레벨에서 막히는 경우

조직 보안 정책에서 GitHub Actions의 패키지 쓰기를 제한하는 경우가 있습니다.

  • Org settings → Packages → Actions 접근 정책
  • 패키지(컨테이너) 가시성/권한 설정

이 경우 워크플로를 고쳐도 계속 403/denied가 나며, 조직 정책 수정 또는 **PAT(개인 액세스 토큰)**로 우회해야 합니다.

3) Docker Hub / ECR push가 실패: 자격증명 스코프 문제

3.1 Docker Hub

  • 증상: 로그인은 됐는데 push에서 denied: requested access
  • 원인: 리포지토리 이름/네임스페이스 불일치, 권한 없는 org repo로 push
- name: Login to Docker Hub
  uses: docker/login-action@v3
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    push: true
    tags: ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest

3.2 AWS ECR

  • 증상: no basic auth credentials, denied: User is not authorized
  • 원인: OIDC/IAM Role 권한 부족 또는 ECR 로그인 누락
permissions:
  id-token: write
  contents: read

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

- name: Login to ECR
  uses: aws-actions/amazon-ecr-login@v2

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    push: true
    tags: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:${{ github.sha }}

IAM 정책에는 최소한 ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload 등이 필요합니다.

4) "Cannot connect to the Docker daemon" / docker.sock permission denied

4.1 GitHub-hosted runner(ubuntu-latest)에서는 보통 발생하지 않음

기본적으로 Docker 사용이 가능하지만, 다음 상황에서 터집니다.

  • 컨테이너 잡(container:) 안에서 Docker를 실행하려는 경우
  • self-hosted runner에서 docker 그룹/권한이 잘못된 경우

4.2 self-hosted runner 해결 체크리스트

  1. 러너 실행 유저가 docker 그룹에 속해 있는지
id
getent group docker
sudo usermod -aG docker <runner-user>
  1. docker 데몬이 실행 중인지
sudo systemctl status docker
  1. 소켓 권한
ls -l /var/run/docker.sock

> 주의: 그룹 추가 후에는 세션 재로그인(또는 러너 서비스 재시작)이 필요합니다.

4.3 컨테이너 잡에서 Docker가 필요하면: DinD 또는 BuildKit 사용

컨테이너 잡 내부에서 Docker를 쓰면 소켓이 없어서 막히는 경우가 많습니다. 가능하면 docker/build-push-action + Buildx를 쓰고, 정말 필요하면 DinD를 명시합니다.

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      docker:
        image: docker:24-dind
        options: >-
          --privileged
        ports:
          - 2375:2375
    env:
      DOCKER_HOST: tcp://localhost:2375
    steps:
      - uses: actions/checkout@v4
      - run: docker version

5) 빌드 중 파일 permission denied: COPY/RUN 단계에서 터지는 케이스

5.1 전형적인 증상

  • COPY failed: file not found or excluded by .dockerignore (권한과 비슷하게 보이지만 다른 문제)
  • chmod: changing permissions of '...': Operation not permitted
  • EACCES: permission denied (Node/Gradle 등 빌드 툴)

5.2 원인 1: 워크스페이스 파일 권한이 깨짐(특히 self-hosted)

self-hosted 러너에서 이전 잡이 root로 파일을 만들어두면 다음 잡에서 일반 유저가 접근 못합니다.

해결: 체크아웃 직후 워크스페이스 소유권 정리(필요 시)

- uses: actions/checkout@v4

- name: Fix workspace permissions (self-hosted only)
  if: ${{ runner.os == 'Linux' }}
  run: |
    sudo chown -R $(id -u):$(id -g) "$GITHUB_WORKSPACE"
    sudo chmod -R u+rwX "$GITHUB_WORKSPACE"

5.3 원인 2: Dockerfile에서 non-root 사용 시 디렉터리 권한 미설정

컨테이너 런타임을 non-root로 돌리는 건 좋은데, 빌드/실행 시 쓰기 디렉터리를 안 열어두면 터집니다.

FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .

# non-root
RUN addgroup -S app && adduser -S app -G app \
  && chown -R app:app /app

USER app
CMD ["node", "server.js"]

5.4 원인 3: Git 파일 모드/실행권한 문제

쉘 스크립트를 실행하는데 Permission denied가 나면 실행 비트가 빠졌을 수 있습니다.

git update-index --chmod=+x scripts/deploy.sh

6) actions/cache, build cache에서 권한/키 문제로 간접 실패

캐시가 “권한 오류”를 직접 내기보다는, 캐시 미스 → 빌드 재실행 → 다른 단계에서 권한 문제가 폭발하는 식으로 나타나기도 합니다. Docker 레이어 캐시를 쓴다면 캐시 구성도 함께 점검하세요.

Buildx의 GHA 캐시 예시:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build (with cache)
  uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha
    cache-to: type=gha,mode=max

7) PR에서만 실패: fork PR의 토큰 권한 제한

fork에서 올라온 PR은 보안상 secrets가 주입되지 않거나, GITHUB_TOKEN 권한이 제한됩니다. 그래서 main 브랜치에서는 되는데 PR에서만 denied가 납니다.

7.1 해결 전략

  • PR에서는 빌드만 하고 push는 하지 않는다.
  • push는 push 이벤트(내부 브랜치)에서만 수행.
on:
  pull_request:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }}

8) 최소 권한(Least Privilege) 기준으로 권한 설계하기

권한 문제를 막겠다고 무작정 토큰 권한을 넓히면, 나중에 보안 감사/침해 사고에서 문제가 됩니다. 권장 패턴은 다음입니다.

  • GHCR push가 필요한 워크플로에만:
    • permissions: packages: write, contents: read
  • AWS OIDC를 쓰는 워크플로에만:
    • permissions: id-token: write, contents: read
  • PR 워크플로에는 secrets/push 동작을 넣지 않기

9) 실전 템플릿: GHCR에 안전하게 build & push

아래 템플릿은 권한/이벤트 분리를 포함해 permission 이슈를 가장 덜 일으키는 형태입니다.

name: docker-cicd

on:
  pull_request:
  push:
    branches: [ main ]

permissions:
  contents: read
  packages: write

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

      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR (push only)
        if: ${{ github.event_name == 'push' }}
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build (PR) / Build+Push (main)
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: |
            ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }}
            ghcr.io/${{ github.repository_owner }}/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

10) 마무리: "permission"은 대개 설정의 경계에서 터진다

GitHub Actions + Docker CI/CD에서 permission 오류는 코드의 문제가 아니라 **경계면(토큰·레지스트리·러너·파일시스템)**에서 터지는 경우가 대부분입니다. 따라서 다음 순서로 보면 빠르게 해결됩니다.

  1. 로그가 인증(401/403)인지, 로컬 권한(EACCES/docker.sock)인지 분기
  2. 레지스트리 push라면 permissions: packages: write부터 확인
  3. self-hosted라면 docker 그룹/소켓/워크스페이스 소유권 확인
  4. PR(fork) 이벤트에서는 push/secret을 분리
  5. 캐시를 쓰면 캐시 설정도 함께 점검

캐시가 꼬여서 빌드 단계가 불필요하게 반복되고, 그 과정에서 권한 문제가 더 자주 드러나는 경우도 많습니다. 캐시 관련 이슈는 위에서 링크한 글(GitHub Actions 캐시 안 먹힘 원인 7가지)을 함께 확인하면 재발 방지에 도움이 됩니다.