- Published on
GitLab CI에서 Kaniko Docker 빌드 403 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 Docker-in-Docker(DinD)를 쓰기 어렵거나 보안 정책상 privileged를 열 수 없을 때, GitLab CI에서 Kaniko는 사실상 표준 대안입니다. 그런데 Kaniko로 이미지 빌드/푸시를 붙이는 순간, 로그에 403 Forbidden이 튀어나오며 파이프라인이 멈추는 경우가 많습니다. 문제는 403이 인증 실패(401) 와 달리 “권한/정책/엔드포인트/토큰 스코프/레지스트리 동작” 등 다양한 원인으로 발생한다는 점입니다.
이 글에서는 GitLab CI + Kaniko 조합에서 발생하는 403을 증상별로 분류하고, GitLab Container Registry와 AWS ECR 기준으로 바로 적용 가능한 .gitlab-ci.yml 예제와 점검 체크리스트를 제공합니다.
> 참고로, 레지스트리 인증 만료/토큰 이슈는 쿠버네티스에서도 동일한 패턴으로 터집니다. 운영에서 ImagePullBackOff가 함께 보인다면 Kubernetes ImagePullBackOff - ECR 인증 만료 해결도 같이 확인해두면 좋습니다.
Kaniko에서 403이 뜨는 대표 증상
Kaniko 로그는 대략 아래 시점에서 403이 발생합니다.
- 푸시 단계:
error pushing image: failed to push to destination ... 403 Forbidden - 레지스트리 인증 확인 단계:
GET https://registry.../v2/ ... 403 - 캐시(push cache) 단계:
--cache=true사용 시 캐시 레포지토리에 푸시하다가 403
핵심은 “빌드는 되는데 푸시에서만 403”인지, “레지스트리 API 조회부터 403”인지에 따라 원인이 갈립니다.
원인 1) GitLab CI 변수/토큰 스코프가 레지스트리 푸시에 부족
가장 흔한 케이스: CI_JOB_TOKEN만 믿고 푸시하려는 경우
GitLab은 CI_JOB_TOKEN으로 레지스트리 접근을 지원하지만, 프로젝트/그룹 설정, 보호 브랜치/태그 정책, 외부 프로젝트 접근 여부에 따라 푸시 권한이 제한될 수 있습니다. 특히 다음 상황에서 403이 자주 납니다.
- 다른 프로젝트의 레지스트리에 푸시 (Cross-project)
- Protected branch/tag에서만 실행되도록 설정했는데 토큰 권한이 제한
- 그룹/프로젝트 레벨에서 job token scope 제한
해결 방향
- 같은 프로젝트 레지스트리에 푸시:
CI_REGISTRY_USER=gitlab-ci-token,CI_REGISTRY_PASSWORD=$CI_JOB_TOKEN조합을 우선 사용 - 크로스 프로젝트/더 강한 권한 필요: Deploy Token 또는 Personal Access Token(PAT) 을 사용
GitLab Container Registry 푸시용 Kaniko 설정 예시
build-image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
variables:
DOCKERFILE: Dockerfile
CONTEXT: $CI_PROJECT_DIR
IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
script:
- mkdir -p /kaniko/.docker
- |
cat > /kaniko/.docker/config.json <<EOF
{
"auths": {
"${CI_REGISTRY}": {
"username": "${CI_REGISTRY_USER}",
"password": "${CI_REGISTRY_PASSWORD}"
}
}
}
EOF
- /kaniko/executor \
--context "$CONTEXT" \
--dockerfile "$DOCKERFILE" \
--destination "$IMAGE" \
--snapshotMode=redo \
--verbosity=info
rules:
- if: $CI_COMMIT_BRANCH
여기서 중요한 포인트:
entrypoint: [""]로 Kaniko 기본 엔트리포인트를 비활성화(스크립트가 정상 실행되도록)config.json에 레지스트리 호스트를 정확히CI_REGISTRY로 매핑CI_REGISTRY_USER/CI_REGISTRY_PASSWORD는 GitLab이 기본 제공하지만, 상황에 따라 직접 주입해야 할 수 있음
> 만약 위 설정 그대로인데도 403이면, 토큰 스코프/정책 문제일 확률이 큽니다. 이때는 Deploy Token(Package/Registry read/write)로 바꾸는 것이 가장 빠릅니다.
원인 2) 레지스트리 주소가 미묘하게 달라서 다른 호스트로 인증이 나감
Kaniko의 Docker config는 호스트 문자열이 1글자라도 다르면 다른 레지스트리로 인식합니다.
예를 들어 아래는 서로 다른 키로 취급됩니다.
registry.gitlab.comhttps://registry.gitlab.comregistry.gitlab.com:443
증상은 “로그인(config.json)은 넣었는데도 403/401이 난다”입니다.
해결 체크
config.json의 auths 키는 스킴 없이 호스트:포트 형태로 통일- destination도 동일한 호스트로 시작하는지 확인
{
"auths": {
"registry.gitlab.com": {
"username": "gitlab-ci-token",
"password": "<token>"
}
}
}
원인 3) 캐시 리포지토리 권한(특히 --cache=true)이 따로 필요
Kaniko는 --cache=true를 켜면 레이어 캐시를 별도 레포지토리에 푸시합니다. 이때 --cache-repo를 지정하지 않으면 destination 기반으로 추론하거나 기본 규칙을 타는데, 조직 정책상 해당 경로가 막혀 403이 날 수 있습니다.
해결
- 캐시를 쓰려면 명시적으로 cache repo를 지정하고, 그 경로에 대한 push 권한을 부여합니다.
script:
- /kaniko/executor \
--context "$CI_PROJECT_DIR" \
--dockerfile Dockerfile \
--destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" \
--cache=true \
--cache-repo "$CI_REGISTRY_IMAGE/cache" \
--verbosity=info
권한이 애매하면 캐시를 끄고(성공 우선) 원인을 좁히는 것도 좋습니다.
원인 4) GitLab Registry 경로/프로젝트 네임스페이스 불일치
GitLab Container Registry는 보통 registry.gitlab.com/<group>/<project> 형태입니다. 그런데 destination에 태그를 붙이면서 경로가 잘못되면, 존재하지 않거나 권한 없는 repo로 푸시를 시도하게 되어 403이 뜹니다.
해결 체크
CI_REGISTRY_IMAGE를 그대로 쓰는 것이 가장 안전- 커스텀 네이밍이 필요하면 실제 레지스트리 UI에서 repo 경로를 확인
# 안전한 패턴
--destination "$CI_REGISTRY_IMAGE:${CI_COMMIT_REF_SLUG}"
원인 5) ECR에서 403: 인증 토큰은 맞는데 리소스 정책/IAM이 막는 경우
ECR은 인증 자체는 aws ecr get-login-password로 잘 되는데도, 다음 이유로 403이 발생합니다.
- 레포지토리 정책(Repository policy)에서 principal 제한
- KMS 암호화 사용 시 KMS 권한 부족
ecr:PutImage,ecr:InitiateLayerUpload,ecr:UploadLayerPart,ecr:CompleteLayerUpload권한 누락- 올바른 계정/리전이 아닌 ECR endpoint로 푸시
쿠버네티스에서 ECR 관련 인증/만료가 얽히면 증상이 더 복잡해지는데, 이 부분은 Kubernetes ImagePullBackOff - ECR 인증 만료 해결에서 함께 다룬 패턴과 연결됩니다.
GitLab CI + Kaniko + ECR 예시(권장: IRSA/런너 IAM 역할 기반)
아래는 AWS CLI로 ECR 토큰을 받아 Kaniko config에 주입하는 방식입니다.
build-ecr:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
variables:
AWS_REGION: ap-northeast-2
ECR_REGISTRY: "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com"
IMAGE: "$ECR_REGISTRY/my-app:$CI_COMMIT_SHA"
before_script:
- apk add --no-cache aws-cli
- aws --version
script:
- mkdir -p /kaniko/.docker
- |
PASS=$(aws ecr get-login-password --region "$AWS_REGION")
cat > /kaniko/.docker/config.json <<EOF
{
"auths": {
"${ECR_REGISTRY}": {
"username": "AWS",
"password": "${PASS}"
}
}
}
EOF
- /kaniko/executor \
--context "$CI_PROJECT_DIR" \
--dockerfile Dockerfile \
--destination "$IMAGE" \
--verbosity=info
여기서 403이 계속되면, 거의 항상 IAM/ECR 리소스 정책 문제입니다. 최소 권한 기준으로는 아래 액션들이 필요합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "*"
}
]
}
레포지토리 정책으로 계정 간 접근을 제어하고 있다면, IAM만 맞춰도 403이 납니다(리소스 정책 우선).
원인 6) 프록시/미러/사설 레지스트리에서 HEAD/PUT 메서드 차단
기업망에서 레지스트리 앞단에 프록시(Nginx, Artifactory, Harbor proxy cache 등)가 있을 때, 다음과 같은 보안 설정이 403을 만들 수 있습니다.
HEAD /v2/...차단- 큰 레이어 업로드(Chunked upload) 제한
- 특정 User-Agent 차단
- HTTP → HTTPS 리다이렉트 정책이 Kaniko와 충돌
해결 힌트
- 프록시 로그에서 403의 차단 룰/위치(location) 를 확인
- Kaniko verbosity를
debug로 올려 어떤 메서드에서 막히는지 확인
/kaniko/executor --verbosity=debug ...
빠른 진단 체크리스트(10분 컷)
1) 403이 “푸시”에서만 발생하는가?
- 빌드 단계는 정상인데
Pushing image to ...에서만 403 → 권한/정책/경로/캐시 repo
2) destination 경로가 정확한가?
- GitLab이면
CI_REGISTRY_IMAGE사용을 최우선
3) config.json의 auths 키가 정확한 호스트인가?
https://붙이지 말 것- 포트가 있으면 destination과 일치시킬 것
4) 캐시를 켰다면 cache repo 권한이 있는가?
--cache=true잠시 끄고 재시도해 원인 분리
5) 토큰 스코프/정책(Protected branch, job token scope)을 확인했는가?
- 필요하면 Deploy Token/PAT로 전환
운영 팁: “403이 나도 원인을 남기는” 로그 설계
CI에서 403은 재현이 어려운 편이라, 실패 시점의 정보를 최대한 남기는 게 좋습니다.
--verbosity=debug는 임시로만(민감정보 노출 주의)- destination, registry host, cache repo를 echo로 출력
- GitLab 변수(
CI_REGISTRY,CI_REGISTRY_IMAGE)를 그대로 쓰고, 커스텀 문자열 조합을 최소화
또한 인증/권한 문제는 종종 “토큰은 맞는데 정책이 막는” 형태로 나타납니다. AWS 쪽에서 403이 반복되고, 같은 클러스터/런너에서 다른 AWS API는 되는데 특정 서비스만 403이라면 권한 경계가 어디인지 빠르게 좁혀야 합니다. 비슷한 접근으로 IRSA는 되는데 S3만 403인 케이스를 진단하는 방법은 EKS IRSA는 되는데 S3만 403? 30분 진단도 참고할 만합니다.
결론
GitLab CI에서 Kaniko로 Docker 이미지를 빌드/푸시할 때의 403은 대개 아래 3가지로 수렴합니다.
- 토큰/권한 부족(job token scope, protected 정책, IAM/ECR 정책)
- 레지스트리 호스트/경로 불일치(config.json auths 키와 destination 불일치)
- 캐시 repo 또는 프록시 정책(cache 푸시 권한, HEAD/PUT 차단)
위의 예제 설정을 기준으로 destination과 auth 구성을 단순화하고, 캐시를 분리해 원인을 좁히면 대부분 1~2회 파이프라인 반복으로 해결됩니다.