Published on

Jenkins Docker 에이전트 Permission denied 7가지 해결

Authors

서론

Jenkins에서 Docker 에이전트(예: agent { docker { ... } }, Docker Plugin의 동적 에이전트, 혹은 Kubernetes 플러그인으로 띄운 컨테이너)가 빌드를 수행할 때 가장 흔히 부딪히는 오류가 permission denied입니다. 문제는 로그에 찍히는 메시지가 비슷해 보여도 실제 원인은 Docker 소켓 권한, 워크스페이스 UID/GID 불일치, 컨테이너 파일시스템 마운트 옵션, SELinux/AppArmor, rootless Docker, Kubernetes securityContext, 네트워크/레지스트리 인증서 등으로 다양하다는 점입니다.

이 글은 “어디서 권한이 막혔는지”를 빠르게 분류하고, 현장에서 바로 적용 가능한 해결책 7가지를 제공합니다. (쿠버네티스에서 에이전트를 쓰는 경우라면 인증서 이슈도 종종 x509로 터지니, 필요하면 Kubernetes ErrImagePull x509 인증서 오류 해결도 함께 참고하세요.)


0. 먼저: permission denied 유형부터 분류하기

아래 3가지만 먼저 확인하면 원인의 70%가 갈립니다.

  1. 무엇에 대한 권한 오류인가?

    • Docker 데몬 소켓: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
    • 파일/디렉터리: Permission denied on workspace, .git, target/, node_modules/
    • 실행 권한: ./gradlew: Permission denied, script.sh: Permission denied
  2. 누가 실행하는가?

    • 컨테이너 내부 사용자: id, whoami
  3. 어디에 쓰려 하는가?

    • 마운트된 경로인지(호스트/볼륨), 컨테이너 레이어인지

파이프라인에서 최소한 아래를 한 번 찍어두면 트러블슈팅 속도가 빨라집니다.

pipeline {
  agent any
  stages {
    stage('debug-perms') {
      steps {
        sh '''
          set -eux
          whoami || true
          id || true
          umask || true
          pwd
          ls -al
          df -h . || true
        '''
      }
    }
  }
}

1) Docker 소켓(/var/run/docker.sock) 권한 문제

증상

  • docker ps만 쳐도 실패
  • 로그 예:
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

원인

컨테이너/에이전트 프로세스가 Docker 데몬 소켓에 접근할 권한이 없습니다. 보통 소켓은 root:docker 소유이며 0660입니다.

해결책

A. 에이전트를 docker 그룹 GID로 실행

호스트에서 docker 그룹 GID 확인:

getent group docker
# 예: docker:x:998:ubuntu

Jenkins Docker 에이전트 실행 시 그룹을 맞춥니다(예시는 Docker run).

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --group-add 998 \
  jenkins-agent-image:latest

B. (권장도 낮음) 소켓 권한을 느슨하게

sudo chmod 666 /var/run/docker.sock

보안상 위험하므로 임시 진단용으로만 사용하세요.


2) Jenkins 워크스페이스 UID/GID 불일치(마운트된 디렉터리)

증상

  • git checkout 또는 빌드 산출물 생성 시 실패
  • cannot create directory ... Permission denied
  • 호스트/볼륨에 마운트된 경로에서만 발생

원인

호스트의 워크스페이스 디렉터리 소유자(UID/GID)와 컨테이너 내부에서 빌드를 수행하는 사용자 UID/GID가 다릅니다. 특히 Jenkins 컨트롤러/에이전트가 jenkins(1000)인데, 컨테이너는 root(0) 또는 반대로 구성된 경우가 흔합니다.

해결책

A. 컨테이너를 호스트/볼륨 소유자 UID로 실행

# 호스트에서 워크스페이스 소유 UID/GID 확인
stat -c '%u %g %n' /var/lib/jenkins/workspace

# 컨테이너 실행 시 동일 UID/GID로
 docker run --rm \
  -u 1000:1000 \
  -v /var/lib/jenkins/workspace:/workspace \
  -w /workspace \
  agent:latest

B. 이미지 빌드 단계에서 사용자/그룹을 고정

FROM eclipse-temurin:21-jdk
RUN groupadd -g 1000 jenkins && useradd -m -u 1000 -g 1000 jenkins
USER jenkins
WORKDIR /home/jenkins

C. Kubernetes라면 fsGroup로 볼륨 권한 맞추기

securityContext:
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000

3) 실행 권한(Executable bit) 문제: gradlew, mvnw, 스크립트

증상

  • ./gradlew: Permission denied
  • script.sh: Permission denied

원인

저장소에 실행 비트가 없거나(특히 Windows에서 커밋), 체크아웃 후 권한이 변형되었거나, 마운트된 파일시스템이 noexec로 걸려 실행이 차단됩니다.

해결책

A. 실행 비트 부여

stage('fix-exec') {
  steps {
    sh '''
      set -eux
      chmod +x gradlew || true
      chmod +x mvnw || true
      find . -maxdepth 2 -name "*.sh" -print -exec chmod +x {} \;
    '''
  }
}

B. noexec 마운트 여부 확인

mount | grep workspace || true
# 또는
findmnt -T .

noexec라면 해당 볼륨/마운트 옵션을 변경하거나, 실행 파일을 컨테이너 레이어(예: /tmp)로 복사 후 실행하세요.

cp ./gradlew /tmp/gradlew && /tmp/gradlew tasks

4) SELinux / AppArmor 정책으로 인한 차단

증상

  • 파일 권한은 맞는데도 Permission denied
  • 특정 경로 마운트 시에만 재현
  • RHEL/CentOS/Fedora 계열(SELinux Enforcing)에서 빈번

원인

리눅스 DAC(일반 파일 권한)만으로는 허용되어도, SELinux/AppArmor가 컨테이너의 접근을 차단할 수 있습니다.

해결책(SELinux)

A. 볼륨 마운트에 :Z 또는 :z 사용

docker run --rm \
  -v /var/lib/jenkins/workspace:/workspace:Z \
  agent:latest
  • :Z는 단일 컨테이너용 라벨
  • :z는 다중 컨테이너 공유 라벨

B. 진단용으로 Enforcing 확인

getenforce
# Enforcing이면 영향 가능

AppArmor는 프로파일에 따라 다르며, 필요 시 해당 컨테이너에 맞는 프로파일 조정이 필요합니다.


5) Rootless Docker/Podman 환경에서의 권한 문제

증상

  • Docker 소켓 경로가 다름
  • permission denied 또는 cannot connect to the Docker daemon

원인

Rootless Docker는 일반적으로 시스템 소켓(/var/run/docker.sock)이 아니라 사용자 런타임 디렉터리 아래 소켓을 사용합니다.

예:

  • unix:///run/user/1000/docker.sock

해결책

A. 올바른 소켓을 마운트하고 DOCKER_HOST 지정

export DOCKER_HOST=unix:///run/user/1000/docker.sock

docker run --rm \
  -e DOCKER_HOST=unix:///run/user/1000/docker.sock \
  -v /run/user/1000/docker.sock:/run/user/1000/docker.sock \
  agent:latest docker ps

B. Jenkins 노드/에이전트가 어떤 Docker를 쓰는지 명확히 분리

  • 시스템 Docker(루트) 기반 노드
  • rootless Docker 기반 노드

혼용하면 “어떤 소켓을 바라보는지”가 뒤섞여 재현이 들쭉날쭉해집니다.


6) Kubernetes 에이전트의 securityContext로 인해 쓰기 불가

증상

  • Kubernetes 플러그인으로 띄운 에이전트에서만 실패
  • /home/jenkins/agent 또는 /workspace 쓰기 실패

원인

Pod Security(또는 PSA), 조직의 보안 정책으로 runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false 등이 강제되며, 이미지 기본 사용자/디렉터리 권한과 충돌합니다.

해결책

A. 에이전트 이미지의 작업 디렉터리를 쓰기 가능하게 설계

FROM jenkins/inbound-agent:latest
USER root
RUN mkdir -p /home/jenkins/agent && chown -R jenkins:jenkins /home/jenkins
USER jenkins

B. Pod 템플릿에 명시적으로 권한 설정

securityContext:
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000
  allowPrivilegeEscalation: false
containers:
- name: jnlp
  image: your-agent:latest
  volumeMounts:
  - name: workspace
    mountPath: /home/jenkins/agent
volumes:
- name: workspace
  emptyDir: {}

emptyDir는 기본적으로 쓰기 가능해 권한 이슈를 줄여줍니다. 반면 NFS/PV를 붙이면 UID/GID/권한 매핑 문제가 다시 등장할 수 있습니다.

쿠버네티스 운영 이슈를 함께 다루는 관점에서는 Kubernetes CNI IP 부족으로 Pod Pending 해결 가이드처럼 “에이전트가 아예 뜨지 않는” 케이스도 종종 섞여 들어오니, 에이전트 생성/스케줄링 단계와 빌드 단계 문제를 분리해 보세요.


7) 캐시/도구 디렉터리 권한: npm, pip, gradle, docker buildx

증상

  • 빌드는 시작되는데 중간에 캐시 쓰기에서 실패
  • 예:
    • EACCES: permission denied, mkdir '/.npm'
    • Could not create service of type FileAccessTimeJournal ... Permission denied
    • pip cache dir ... Permission denied

원인

컨테이너가 비루트로 실행되는데, 도구 기본 캐시 경로가 / 아래이거나(예: /.npm), 홈 디렉터리가 쓰기 불가하거나, 이전 빌드가 root로 생성한 캐시가 남아 소유권 충돌이 납니다.

해결책

A. 캐시 경로를 워크스페이스 하위로 강제

stage('build') {
  environment {
    NPM_CONFIG_CACHE = "${WORKSPACE}/.npm-cache"
    GRADLE_USER_HOME = "${WORKSPACE}/.gradle"
    PIP_CACHE_DIR = "${WORKSPACE}/.pip-cache"
  }
  steps {
    sh '''
      set -eux
      mkdir -p "$NPM_CONFIG_CACHE" "$GRADLE_USER_HOME" "$PIP_CACHE_DIR"
      npm ci
      ./gradlew test
      pip install -r requirements.txt
    '''
  }
}

B. 기존 캐시 소유권 정리(특히 root로 실행했던 히스토리가 있을 때)

sudo chown -R 1000:1000 /var/lib/jenkins/workspace/your-job

C. Docker BuildKit/buildx가 생성하는 디렉터리 권한 확인

BuildKit은 내부적으로 캐시/상태 디렉터리를 만들 수 있습니다. 에이전트가 비루트라면 DOCKER_CONFIG, XDG_RUNTIME_DIR 등을 명시해 쓰기 가능 위치로 옮기는 것이 안전합니다.


재발 방지 체크리스트(운영 관점)

  • 에이전트 실행 사용자(UID/GID)를 표준화하고, 워크스페이스/캐시 경로를 그에 맞춰 설계했는가?
  • Docker 소켓 접근이 필요한 빌드인지(도커-인-도커, 도커-아웃-오브-도커) 명확히 구분했는가?
  • SELinux Enforcing 환경에서 볼륨 라벨(:Z/:z)을 고려했는가?
  • Kubernetes라면 securityContext(runAsUser/fsGroup)를 Pod 템플릿에 명시했는가?

권한 문제는 종종 “인증/인가” 문제(403 등)와 같이 묶여 보고되기도 합니다. 클라우드 환경에서 권한(AccessDenied/403) 트러블슈팅이 필요하다면 EKS IRSA 설정했는데 AccessDenied 뜰 때 점검처럼 계층별로 원인을 분리하는 접근이 유사하게 도움이 됩니다.


결론

Jenkins Docker 에이전트의 permission denied는 단일 해결책이 아니라, (1) Docker 소켓, (2) 워크스페이스 UID/GID, (3) 실행 비트/noexec, (4) SELinux/AppArmor, (5) rootless 소켓 경로, (6) Kubernetes securityContext, (7) 도구 캐시 경로/소유권 7가지 축으로 나눠 보면 빠르게 수습할 수 있습니다.

가장 추천하는 운영 표준은 다음 두 가지입니다.

  • “에이전트는 항상 동일 UID/GID로 실행” + “워크스페이스/캐시는 항상 쓰기 가능한 경로로 고정”
  • Docker가 필요하면 소켓 접근 방식을 명확히 하고(가능하면 최소권한), 쿠버네티스에서는 fsGroup로 볼륨 권한을 통일

이 표준만 잡아도 permission denied의 대부분은 재발하지 않습니다.