GitHub Actions artifact attestation 검증: 배포 파일 출처 확인하기
GitHub Actions artifact attestation은 다운로드한 파일의 digest를 서명된 provenance와 연결해, 어느 저장소와 워크플로에서 만들어졌는지 확인하는 장치다. 배포 전에 gh attestation verify를 실행하면 파일 변조뿐 아니라 기대한 repository, signer workflow, source ref와의 불일치도 정책으로 차단할 수 있다.
이 글은 2026년 8월 4일 공식 문서와 GitHub CLI 2.97.0을 기준으로 한다. 공개 cli/cli 릴리스 tarball을 읽기 전용으로 검증해 정상본, 잘못된 branch 조건, 잘못된 signer workflow, 1바이트 변조본을 비교했다. private/internal repository, GHES, OCI, SBOM, reusable workflow와 완전한 air-gap 환경은 직접 시험하지 않았다.

검증 성공을 출처 확인으로 만들려면 기대값을 고정해야 한다
가장 단순한 명령은 다음과 같다.
gh attestation verify FILE -R OWNER/REPO
그러나 repository만 맞는다는 조건은 실제 배포 게이트로는 넓다. GitHub CLI 매뉴얼에 따르면 검증기는 artifact digest와 statement subject의 연결, 서명과 timestamp, 신뢰 root, 기본 SLSA provenance predicate, 인증서의 repository 또는 owner와 signer identity를 확인한다. 최소 조건으로 --owner나 --repo가 필요하지만, 특정 배포 파일의 출처를 확인하려면 범위가 더 좁은 --repo를 쓰고 정확한 signer workflow와 source ref까지 고정하는 편이 낫다.
배포 정책에 사용할 명령은 다음 형태가 된다. 아래 저장소, workflow, tag와 commit 값은 예시 자리 표시자이므로 실제 조직이 승인한 값으로 바꿔야 한다.
gh attestation verify release.tar.gz \
--repo OWNER/REPO \
--signer-workflow OWNER/REPO/.github/workflows/release.yml \
--source-ref refs/tags/v1.2.3 \
--source-digest EXPECTED_COMMIT_SHA \
--format json
branch 이름은 이동할 수 있다. 고정된 릴리스 판정이 필요하면 허용 tag 정책과 --source-digest, 필요할 경우 --signer-digest까지 함께 고정한다. self-hosted build를 허용하지 않는 조직이라면 --deny-self-hosted-runners도 정책에 추가할 수 있다. reusable workflow가 서명자라면 caller workflow가 아니라 실제 reusable workflow를 --signer-workflow 또는 --signer-repo에 지정해야 한다.
동일한 파일과 bundle로 정상·불일치·변조를 비교했다
직접 검증 환경은 Ubuntu 24.04 계열 x86_64 환경, GitHub CLI 2.97.0 (2026-07-31)이었다. 대상은 공개 cli/cli 릴리스의 gh_2.97.0_linux_amd64.tar.gz다. 공개 REST digest endpoint에서 attestation metadata와 bundle 두 개를 읽어 로컬 JSONL bundle로 만든 뒤, 같은 CLI와 bundle을 유지하고 파일 또는 정책 조건만 바꿨다.
원본 SHA-256과 정확한 repository·workflow·branch 조건은 다음과 같았다.
$ sha256sum gh_2.97.0_linux_amd64.tar.gz
a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112
$ gh attestation verify gh_2.97.0_linux_amd64.tar.gz -R cli/cli \
--bundle bundle.jsonl \
--signer-workflow cli/cli/.github/workflows/deployment.yml \
--source-ref refs/heads/trunk
exit_code=0
정상 JSON에서는 predicateType=https://slsa.dev/provenance/v1, sourceRepositoryRef=refs/heads/trunk, 대상 파일명과 원본 SHA-256이 subject 목록에서 관측됐다. 전체 인증서와 bundle 원문은 불필요한 식별정보를 줄이기 위해 제외했다.
파일은 그대로 두고 기대 branch만 main으로 바꾸면 ref 불일치가 구체적으로 드러났다.
$ gh attestation verify gh_2.97.0_linux_amd64.tar.gz -R cli/cli \
--bundle bundle.jsonl --source-ref refs/heads/main
Error: expected SourceRepositoryRef to be refs/heads/main, got refs/heads/trunk
exit_code=1
원본의 복사본 끝에 문자 X 1바이트를 추가하자 SHA-256이 달라졌고, 같은 repository와 bundle 조건에서 거부됐다.
$ printf 'X' >> mutated.tar.gz
$ sha256sum mutated.tar.gz
600dcacf501ac8f170a003da32f846390dd7cb6d1ca9f6c425ff6da798ea0f5d
$ gh attestation verify mutated.tar.gz -R cli/cli --bundle bundle.jsonl
Error: verifying with issuer "sigstore.dev"
exit_code=1
잘못된 signer workflow도 종료 코드 1로 실패했다. 같은 입력을 통제해 비교한 결과는 다음과 같다.
| 대상과 정책 | 관측 결과 | 배포 판단 |
|---|---|---|
원본 + 정확한 repo/workflow/trunk |
exit 0 | 다음 보안 검사로 진행 |
원본 + 기대 ref main |
exit 1 | 배포 보류 |
| 원본 + 잘못된 signer workflow | exit 1 | 배포 보류 |
| 1바이트 변조본 + 원본 bundle | exit 1 | 배포 보류 |

변조본의 오류 문구는 digest mismatch를 직접 말하지 않고 포괄적인 issuer verification 오류를 표시했다. 따라서 자동화가 오류 문자열을 파싱해 성공과 실패를 판단하면 CLI 버전이나 출력 변화에 취약하다. 종료 코드 0과 non-zero를 계약으로 삼고, 성공한 경우에만 --format json의 구조화된 결과를 감사 로그에 남기는 편이 안전하다.
인증 실패와 정책 불일치는 모두 막되 원인은 구분한다
공식 매뉴얼은 기본적으로 GitHub API에서 attestation을 가져온다고 설명한다. 하지만 이번 무인증 환경에서 bundle 없이 gh attestation verify FILE -R cli/cli를 실행하자 로그인 또는 token이 필요하다는 메시지와 종료 코드 4가 발생했다. 공개 repository라면 언제나 인증 없이 자동 조회된다고 일반화할 수 없었다.
실험에서는 공개 REST digest endpoint가 HTTP 200과 attestation 두 개를 반환해 이를 로컬 bundle로 제공했다. 다만 REST 응답에 attestation이 존재한다는 사실 자체는 검증 성공이 아니다. GitHub REST API 문서는 signature, timestamp와 signer identity를 암호학적으로 검증해야 한다고 명시한다. private repository에서는 read access와 fine-grained token의 attestations:read 권한도 필요하다.
운영 게이트에서는 인증 또는 API 장애와 provenance 정책 불일치를 로그상 다른 원인으로 기록하되, 둘 다 자동 우회하지 않아야 한다. attestation을 가져오지 못했거나 repo, workflow, ref, digest 중 하나라도 맞지 않으면 배포를 보류한다. 공식 문서가 설명한 subject digest, repository·signer·ref 정책과 로컬 --bundle 검증은 직접 관측과 일치했지만, 무인증 자동 조회 동작은 이번 환경에서 재현되지 않았다.
배포 게이트는 다섯 단계로 구성할 수 있다
- HTTPS로 artifact를 다운로드한다. 다운로드 경로의 신뢰만으로 provenance 검증을 대체하지 않는다.
- 승인된
OWNER/REPO, 정확한 signer workflow, 허용 ref 또는 tag, 가능한 경우 source와 signer commit digest, predicate type을 코드 리뷰 가능한 정책으로 고정한다. gh attestation verify를 실행한다. non-zero이면 인증 장애인지 정책 불일치인지 기록하고 배포를 보류한다.- exit 0이면
--format json에서 검증된 subject digest, certificate identity와 timestamp를 추출해 감사 로그에 남긴다. - 취약점 검사, 승인된 source review와 필요한 별도 통제를 계속 적용한다.
JSON을 정책 엔진에 넘길 때도 신뢰 경계를 구분해야 한다. GitHub는 certificate와 검증된 timestamp만 workflow 작성자가 조작할 수 없는 값이라고 경고한다. build workflow가 장악되면 statement.predicate 내부에 거짓 내용을 넣을 수 있으므로, predicate의 임의 필드 하나를 곧바로 신뢰 근거로 삼지 말고 신뢰하는 reusable workflow와 certificate identity를 결합해야 한다.
Sigstore 서명과 SLSA provenance가 보증하지 않는 것
Sigstore는 ephemeral key, Fulcio의 OIDC identity certificate와 Rekor transparency log를 조합한다. verifier는 인증서 서명, 기대 identity, trust root와 Rekor inclusion proof를 확인해 서명된 대상과 예상 출처의 연결 및 사후 변조 여부를 판단한다. GitHub public repository attestation은 Public Good Instance와 공개 transparency log를 사용하지만, private repository용 GitHub Sigstore instance에는 공개 transparency log가 없다.
SLSA 버전과 수준도 분리해서 읽어야 한다. SLSA 1.2 Build Track에서 L1은 provenance 존재, L2는 hosted platform이 생성·서명한 provenance와 소비자의 authenticity 검증, L3는 build 간 격리와 사용자 build step에서 signing secret에 접근하지 못하게 하는 hardened platform 통제를 요구한다. Attestation 파일 하나가 존재하거나 검증됐다는 사실만으로 L3를 선언할 수 없다.
GitHub는 reusable workflow와 artifact attestations 조합을 두고 SLSA v1.0 Build Level 3 달성을 지원한다고 설명한다. 이는 현재 SLSA 1.2의 수준 요구사항과 같은 문장으로 합쳐 단정할 내용이 아니다. 소비자는 builder를 어디까지 신뢰할지, 어떤 provenance와 certificate identity를 허용할지 별도로 정해야 한다.
무엇보다 attestation은 파일에 악성 코드나 취약점이 없다는 보증이 아니다. 검증 성공은 “승인한 출처와 빌드 경로에 연결되고 전달 뒤 변조되지 않았다”는 판단 근거다. 승인한 workflow 자체가 잘못됐거나 source에 취약점이 있다면 별도 검사가 필요하다.
지원 플랜과 오프라인 검증 조건을 확인한다
2026년 8월 4일 GitHub 사용 문서 기준으로 artifact attestations는 현행 모든 플랜의 repository에서 사용할 수 있다. 다만 Free, Pro, Team은 public repository로 제한되고 private/internal repository에는 GitHub Enterprise Cloud가 필요하다. legacy Bronze, Silver, Gold 플랜에서는 제공하지 않는다.
생성 단계에서는 workflow에 id-token: write, contents: read, attestations: write 권한을 부여하고 빌드 뒤 actions/attest@v4에 subject-path를 전달하는 것이 공식 예다. 권한은 가능한 workflow 또는 job 범위로 제한한다. 이번 검증은 기존 공개 attestation을 읽었을 뿐 repository 설정을 바꾸거나 새 attestation을 생성하지 않았다.
망 분리 환경에서는 온라인 구간에서 gh attestation download ARTIFACT -R OWNER/REPO로 bundle을 받고 gh attestation trusted-root > trusted_root.jsonl로 root를 내보낸다. 이후 artifact, bundle, CLI와 trusted root를 air-gap 환경으로 반입해 --bundle과 --custom-trusted-root로 검증한다. GitHub 오프라인 검증 문서는 새 signed material을 반입할 때마다 trusted root를 새로 만들 것을 권고한다. 기존 root에는 자체 만료일이 없고 이후 key rotation이나 revocation을 자동으로 알지 못하기 때문이다.
이번 로컬 bundle 검증은 trusted-root 자료를 네트워크에서 얻는 동작까지 완전히 차단한 air-gap 시험은 아니다. 또한 단일 공개 저장소와 CLI 한 버전만 비교했고, bundle 획득에는 gh attestation download 대신 공개 REST endpoint를 사용했다. 플랜, action major version, CLI 옵션과 trusted root 운영 방식은 게시 직전 다시 확인해야 한다.
출처·워크플로·ref가 하나라도 다르면 배포를 보류한다
실제 게이트의 기준은 짧다. exit 0만으로 끝내지 않고, 조직이 승인한 repository, signer workflow, ref 또는 tag와 필요한 commit digest가 모두 일치했을 때만 다음 단계로 넘긴다. 조회 실패와 non-zero 결과는 자동으로 건너뛰지 않는다.
이 방식은 일반 릴리스 파일의 출처와 무결성을 확인하려는 팀에 적합하다. 반대로 취약점 부재나 코드 안전성까지 한 번에 보증하려는 목적에는 맞지 않는다. 먼저 작은 공개 artifact로 정상·정책 불일치·변조 대조군을 만들고, 오류 문자열이 아니라 종료 코드와 검증된 JSON 필드를 기준으로 배포 보류 동작을 시험하는 것이 다음 단계다.
컨테이너 이미지 attestation과 일반 배포 파일 검증의 차이가 필요하면 Docker 이미지 attestation 조회 글을 확인한다.
FAQ
Attestation 검증과 checksum 비교는 무엇이 다른가?
Checksum은 계산한 digest가 기준값과 같은지 확인한다. Artifact attestation 검증은 digest를 서명된 provenance subject와 연결하고, repository, signer workflow, source ref 같은 출처 identity가 기대 정책과 맞는지도 확인한다. 단, 어느 방식도 파일에 취약점이나 악성 코드가 없음을 보증하지 않는다.
--owner와 --repo 중 어느 쪽이 더 엄격한가?
특정 저장소의 배포 파일을 검증한다면 --repo가 더 구체적이다. --owner는 같은 owner 아래의 더 넓은 범위를 허용한다. --repo에 더해 정확한 --signer-workflow와 source ref 또는 digest를 함께 고정해야 의미 있는 배포 경계가 된다.
gh attestation verify는 변조 파일에 어떤 종료 코드를 반환하는가?
GitHub CLI 2.97.0으로 진행한 이번 통제 비교에서는 원본이 exit 0, 끝에 1바이트를 추가한 복사본이 exit 1이었다. 오류 문구는 포괄적이었으므로 자동화는 특정 문자열보다 0/non-zero 종료 상태를 기준으로 삼아야 한다. 다른 버전의 세부 문구까지 같다고 일반화해서는 안 된다.
인터넷이 없는 환경에서는 어떻게 검증하는가?
온라인 환경에서 artifact의 bundle과 최신 trusted root를 준비한 뒤 artifact, CLI와 함께 망 분리 환경으로 반입한다. 그곳에서 --bundle과 --custom-trusted-root를 사용한다. 새 signed material을 반입할 때마다 trusted root도 새로 준비해야 이후 key rotation이나 revocation을 놓칠 위험을 줄일 수 있다.
SLSA provenance가 있으면 artifact가 안전하다고 볼 수 있는가?
그렇지 않다. Provenance는 artifact가 어떤 source와 build instruction에서 만들어졌는지 검증하는 근거다. 악성 코드나 취약점 부재를 뜻하지 않으며, attestation 하나만으로 SLSA L3를 선언할 수도 없다.