- Published on
GitHub Actions로 Docker SBOM·서명 자동화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서플라이체인 보안 요구가 커지면서, 컨테이너 이미지는 단순히 docker build 로 끝나지 않습니다. 최소한 다음 3가지는 CI 단계에서 자동화하는 게 표준이 됐습니다.
- 무엇이 들어갔는지 증명하는 SBOM(Software Bill of Materials)
- 알려진 취약점을 조기에 잡는 이미지 스캔
- 배포 전에 “이 이미지는 우리가 만든 것”임을 증명하는 서명(Signing)
이 글에서는 GitHub Actions만으로 Docker 이미지 빌드, SBOM 생성, 취약점 스캔, Sigstore 기반 서명과 검증까지 자동화하는 구성을 다룹니다. 운영에서 자주 막히는 포인트(권한, 태깅, 아티팩트/어테스테이션, 재현성, 브랜치 전략)도 함께 정리합니다.
참고로 쿠버네티스 운영에서 문제가 터졌을 때 원인을 빠르게 좁히는 방법은 K8s CrashLoopBackOff 원인 10분내 찾는 법도 같이 보면 좋습니다. 서명된 이미지라도 런타임 설정이 잘못되면 장애는 그대로 발생합니다.
목표 아키텍처: 빌드부터 배포 전 검증까지
최종 파이프라인은 아래 흐름을 목표로 합니다.
- 멀티플랫폼 Docker 이미지 빌드 및 레지스트리 푸시(GHCR 예시)
- SBOM 생성(SPDX 또는 CycloneDX)
- 취약점 스캔(예: Trivy)
- 이미지 서명(Sigstore Cosign, OIDC 키리스 방식)
- 서명 검증 및 정책화(배포 파이프라인 또는 클러스터에서 강제)
여기서 핵심은 “SBOM과 서명”을 별도 산출물로 남기는 것이 아니라, 이미지 자체에 연결된 메타데이터(어테스테이션/서명)로 남겨 유통 과정에서 검증 가능하게 만드는 것입니다.
준비물과 전제 조건
- 레지스트리: GitHub Container Registry(권장) 또는 Docker Hub, ECR 등
- GitHub Actions 권한
permissions: id-token: write(Cosign 키리스 서명에 필요)permissions: packages: write(GHCR 푸시에 필요)permissions: contents: read
- 이미지 태깅 전략
- 커밋 SHA 태그(불변)와 릴리스 태그(가독) 병행 권장
왜 OIDC 키리스 서명이 중요한가
전통적 서명은 개인 키를 CI에 넣어야 해서 유출 위험과 키 로테이션 부담이 큽니다. Cosign의 키리스 모드는 GitHub Actions의 OIDC 토큰으로 “누가, 어떤 워크플로에서, 어떤 커밋을 빌드했는지”를 서명에 포함시켜, 키 관리 부담을 크게 줄입니다.
워크플로 예시: 빌드, SBOM, 스캔, 서명
아래 예시는 GHCR에 ghcr.io/OWNER/REPO 형태로 푸시하고, SBOM 생성 후 서명까지 수행합니다.
주의: 본문에서 부등호 문자가 노출되면 MDX 빌드 에러가 날 수 있어, 비교 연산이나 제네릭 표기 등은 모두 인라인 코드로 처리합니다.
name: build-sbom-sign
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=ref,event=branch
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: false
- name: Install Syft (SBOM)
uses: anchore/sbom-action/download-syft@v0
- name: Generate SBOM (SPDX)
run: |
syft "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" \
-o spdx-json=sbom.spdx.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-spdx
path: sbom.spdx.json
- name: Run Trivy image scan
uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
format: "table"
exit-code: "1"
ignore-unfixed: true
vuln-type: "os,library"
severity: "CRITICAL,HIGH"
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image (keyless)
env:
COSIGN_EXPERIMENTAL: "true"
run: |
cosign sign --yes \
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
- name: Verify signature
env:
COSIGN_EXPERIMENTAL: "true"
run: |
cosign verify \
--certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/build-sbom-sign.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
포인트 해설
docker/build-push-action@v6의outputs.digest를 기준으로 이후 단계가 모두 “불변 참조”를 사용합니다.- 태그는 사람이 보기 쉽지만, 재태깅될 수 있습니다.
- digest는 컨텐츠 기반이라 동일 이미지를 정확히 지칭합니다.
- SBOM은 Syft로 생성했습니다. SPDX JSON은 도구 호환성이 좋아 많이 씁니다.
- Trivy는
exit-code: "1"로 설정해 High/Critical 취약점이 있으면 빌드를 실패시킵니다.- 운영에서는 예외 목록,
ignore-unfixed정책, 베이스 이미지 패치 주기까지 함께 설계해야 합니다.
- 운영에서는 예외 목록,
- Cosign은 키리스 서명으로, GitHub OIDC를 사용합니다.
cosign verify에서certificate-identity를 워크플로 경로와 브랜치까지 고정해 “어떤 워크플로가 서명했는지”를 강하게 제한합니다.
SBOM을 “아티팩트”로만 두면 생기는 문제
SBOM을 actions/upload-artifact 로만 남기면 다음 문제가 생깁니다.
- 이미지가 레지스트리로 이동하거나 태그가 바뀌면 SBOM과의 연결이 약해짐
- 배포 시점에 “이 이미지의 SBOM”을 자동으로 찾기 어려움
그래서 실무에서는 SBOM을 이미지와 함께 배포 가능한 형태로 “어테스테이션” 하거나, 레지스트리에 첨부 가능한 방식으로 저장하는 패턴을 선호합니다.
Cosign으로 SBOM 어테스테이션하기
Cosign은 SBOM을 어테스테이션 형태로 이미지 digest에 연결할 수 있습니다.
cosign attest --yes \
--predicate sbom.spdx.json \
--type spdxjson \
"ghcr.io/OWNER/REPO@sha256:..."
검증은 다음처럼 할 수 있습니다.
cosign verify-attestation \
--type spdxjson \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity "https://github.com/OWNER/REPO/.github/workflows/build-sbom-sign.yml@refs/heads/main" \
"ghcr.io/OWNER/REPO@sha256:..."
이렇게 하면 “이미지”와 “SBOM”이 한 덩어리로 유통되기 때문에, 배포 파이프라인이나 클러스터에서 정책적으로 강제하기 쉬워집니다.
실패를 줄이는 운영 팁 7가지
1) digest 기반으로 모든 후속 단계를 고정
스캔, 서명, 어테스테이션, 배포는 모두 image:tag 대신 image@digest 를 사용하세요. 태그는 편의용, digest는 진실의 원천입니다.
2) 브랜치별 정책을 분리
main에서만 서명 허용- PR에서는 빌드와 스캔까지만 수행(푸시는 선택)
- 릴리스 태그(
v1.2.3)에서만 프로덕션 레지스트리로 승격
이렇게 하면 “서명된 이미지 = 배포 후보”라는 의미를 강하게 만들 수 있습니다.
3) 취약점 스캔은 현실적인 게이트로
처음부터 High/Critical을 모두 막으면 레거시 프로젝트는 CI가 계속 깨질 수 있습니다. 단계적으로 다음을 추천합니다.
- 1단계: 리포트만 생성, 추세 관찰
- 2단계: Critical만 차단
- 3단계: High까지 차단, 대신 예외 목록과 만료 정책 도입
예외 목록을 무기한으로 두면 보안 부채가 쌓입니다. TTL 개념이 왜 중요한지는 AutoGPT 메모리 폭주? 벡터DB TTL로 안정화에서 다룬 “만료 정책” 사고방식이 운영 전반에 도움이 됩니다.
4) 재현 가능한 빌드에 신경쓰기
SBOM이 의미 있으려면 “같은 소스는 같은 산출물”로 이어지는 재현성이 중요합니다.
- 패키지 매니저 lockfile 고정
- 베이스 이미지 태그 대신 digest 고정(가능하면)
- 빌드 시각/환경에 따라 변하는 파일 제거 또는 정규화
5) 최소 권한 원칙
GitHub Actions permissions 는 워크플로 단위로 최소화하세요.
- 서명 job에만
id-token: write - 푸시 job에만
packages: write
6) 배포 전 검증을 반드시 넣기
서명은 “했다”로 끝나면 효과가 없습니다. 배포 직전에 검증이 있어야 합니다.
- CD 파이프라인에서
cosign verify강제 - 쿠버네티스에서는 정책 엔진으로 “서명된 이미지만 허용” 강제
7) 장애 대응 관점에서 로그와 증적을 남기기
SBOM/서명 자동화는 컴플라이언스뿐 아니라, 장애 시 원인 분석에도 유용합니다.
- 어떤 커밋이 어떤 베이스 이미지로 빌드됐는지
- 어떤 라이브러리 버전이 포함됐는지
- 특정 CVE가 포함된 이미지가 어디까지 배포됐는지
클러스터에서 실제로 어떤 이미지가 떠 있고 왜 문제가 생겼는지 추적할 때는 노드/컨테이너 로그 수집이 중요합니다. EKS 운영이라면 EKS Bottlerocket 노드 NotReady일 때 로그 수집법도 함께 참고해 두면 좋습니다.
자주 겪는 문제와 해결
Cosign 서명 단계에서 권한 오류
permissions: id-token: write누락이 가장 흔합니다.- 조직 정책으로 OIDC가 제한된 경우, GitHub OIDC 설정과 레포 권한을 확인해야 합니다.
멀티아키텍처 이미지에서 digest 혼동
멀티플랫폼 이미지는 “manifest list digest” 와 “플랫폼별 이미지 digest” 가 다를 수 있습니다. 서명과 검증은 보통 manifest digest에 대해 수행하는 것이 일반적입니다.
워크플로에서 build-push-action 의 outputs.digest 를 그대로 쓰면, 대개 올바른 대상(manifest digest)을 잡습니다. 다만 도구 조합에 따라 달라질 수 있으니, 레지스트리에서 실제 digest를 확인하는 절차를 초기에 한 번 검증하세요.
SBOM이 너무 커서 처리 비용 증가
- 필요 시 SBOM 포맷을 CycloneDX로 바꾸거나
- 개발/PR 단계에서는 SBOM 생성을 생략하고 main에서만 수행하는 식으로 비용을 줄일 수 있습니다.
마무리: “만들기”가 아니라 “유통”을 자동화하자
Docker 이미지를 안전하게 운영하려면, 빌드 산출물을 레지스트리에 올리는 순간부터 이미 “공급망”이 시작됩니다. GitHub Actions로 SBOM 생성과 서명까지 자동화하면 다음이 가능해집니다.
- 누가 어떤 코드로 만든 이미지인지 추적 가능
- 이미지 구성요소(SBOM) 기반의 보안 점검 자동화
- 배포 단계에서 서명 검증으로 무단 이미지 차단
다음 단계로는 (1) Cosign 어테스테이션을 표준화하고, (2) 배포 파이프라인이나 클러스터 정책에서 검증을 강제해 “서명 없는 이미지는 실행 불가”까지 끌고 가는 것을 추천합니다.