Sigstore로 키리스(keyless) 서명·검증 사용하기
Sigstore로 키리스(keyless) 서명·검증 사용하기
소프트웨어 공급망을 보호하려면 빌드한 컨테이너 이미지나 아티팩트가 진짜로 우리가 만든 것인지 검증할 수 있어야 해요. Sigstore 프로젝트는 Cosign이라는 CLI를 제공하는데, GitLab CI/CD로 빌드한 컨테이너 이미지를 키리스 방식으로 서명할 때 쓸 수 있어요. 키리스 서명의 큰 장점은 개인 키를 관리·보호·교체할 필요가 없다는 거예요.
출처: 문서
본문
- Tier: Free, Premium, Ultimate
- Offering: GitLab.com
Cosign은 서명에 쓸 단기 키 쌍을 요청하고, 이를 인증서 투명성 로그에 기록한 뒤 폐기해요. 키는 파이프라인을 실행한 사용자의 OIDC 신원을 이용해 GitLab 서버에서 얻은 토큰으로 생성돼요. 이 토큰에는 토큰이 CI/CD 파이프라인에서 생성됐다는 걸 증명하는 고유한 클레임이 포함돼요. 자세한 내용은 키리스 서명에 대한 Cosign 문서를 참고해요.
GitLab OIDC 클레임과 Fulcio 인증서 확장 간 매핑에 대한 자세한 내용은 OIDC 토큰 클레임을 Fulcio OID로 매핑의 GitLab 열을 참고해요.
자체 호스팅 Sigstore 인프라를 쓰는 GitLab Self-Managed는 자체 호스팅 Sigstore로 아티팩트·컨테이너 이미지 서명을 참고해요.
사전 요건:
- GitLab.com을 사용 중이어야 해요.
- 프로젝트의 CI/CD 설정이 프로젝트 안에 있어야 해요.
Cosign으로 컨테이너 이미지와 빌드 아티팩트 서명·검증하기
Cosign으로 컨테이너 이미지와 빌드 아티팩트를 서명하고 검증할 수 있어요.
사전 요건:
>= 2.0.1버전의 Cosign을 사용해야 해요.
알려진 문제
- CI/CD 설정 파일의
id_tokens부분은 빌드·서명되는 프로젝트에 있어야 해요. AutoDevOps, 다른 저장소에서 포함되는 CI 파일, 하위 파이프라인은 지원되지 않아요. 이 제한을 없애는 작업은 epic 11637에서 추적되고 있어요.
모범 사례:
- 이미지/아티팩트를 빌드하고 서명하는 것을 같은 잡에서 해야 서명되기 전에 변조되는 걸 막을 수 있어요.
- 컨테이너 이미지를 서명할 때는 태그 대신 (불변인) 다이제스트를 서명해요.
GitLab ID 토큰은 Cosign이 키리스 서명에 사용할 수 있어요. 토큰은 aud 클레임이 sigstore로 설정되어야 해요. 토큰을 SIGSTORE_ID_TOKEN 환경 변수에 설정하면 Cosign이 자동으로 사용할 수 있어요.
Cosign 설치 방법은 Cosign 설치 문서를 참고해요.
서명 (Signing)
컨테이너 이미지
Cosign.gitlab-ci.yml 템플릿을 사용하면 GitLab CI에서 컨테이너 이미지를 빌드하고 서명할 수 있어요. 서명은 이미지와 같은 컨테이너 저장소에 자동으로 저장돼요.
include:
- template: Cosign.gitlab-ci.yml
컨테이너 서명에 대해 자세히 알아보려면 Cosign 컨테이너 서명 문서를 참고해요.
빌드 아티팩트
다음 예시는 GitLab CI에서 빌드 아티팩트를 서명하는 방법을 보여줘요. cosign sign-blob이 만들어내는 cosign.bundle 파일을 저장해 두어야 해요. 이 파일은 서명 검증에 사용돼요.
아티팩트 서명에 대해 자세히 알아보려면 Cosign Blob 서명 문서를 참고해요.
build_and_sign_artifact:
stage: build
image: alpine:latest
variables:
COSIGN_YES: "true"
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
before_script:
- apk add --update cosign
script:
- echo "This is a build artifact" > artifact.txt
- cosign sign-blob artifact.txt --bundle cosign.bundle
artifacts:
paths:
- artifact.txt
- cosign.bundle
검증 (Verification)
명령줄 인자
| 이름 | 값 |
|---|---|
--certificate-identity |
Fulcio가 발급한 서명 인증서의 SAN이에요. 이미지/아티팩트가 서명된 프로젝트의 다음 정보로 구성할 수 있어요: GitLab 인스턴스 URL + 프로젝트 경로 + // + CI 설정 경로 + @ + 참조(ref) 경로. |
--certificate-oidc-issuer |
이미지/아티팩트가 서명된 GitLab 인스턴스 URL이에요. 예: https://gitlab.com. |
--bundle |
cosign sign-blob이 만들어낸 bundle 파일이에요. 빌드 아티팩트 검증에만 사용해요. |
서명된 이미지/아티팩트 검증에 대해 자세히 알아보려면 Cosign 검증 문서를 참고해요.
컨테이너 이미지
다음 예시는 GitLab CI에서 서명된 컨테이너 이미지를 검증하는 방법을 보여줘요. 앞서 설명한 명령줄 인자를 사용해요.
verify_image:
image: alpine:3.20
stage: verify
before_script:
- apk add --update cosign docker
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- cosign verify "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" --certificate-identity "https://gitlab.com/my-group/my-project//path/to/.gitlab-ci.yml@refs/heads/main" --certificate-oidc-issuer "https://gitlab.com"
추가 세부 사항:
- 프로젝트 경로와
.gitlab-ci.yml경로 사이의 이중 슬래시는 오타가 아니라 검증이 성공하려면 반드시 필요한 부분이에요. 단일 슬래시를 쓰면 흔히 나는 오류는Error: none of the expected identities matched what was in the certificate, got subjects인데, 그 뒤에 프로젝트 경로와.gitlab-ci.yml경로 사이에 슬래시가 두 개 있는 서명 URL이 표시돼요. - 검증이 서명과 같은 파이프라인에서 일어난다면
--certificate-identity에"${CI_PROJECT_URL}//.gitlab-ci.yml@refs/heads/${CI_COMMIT_REF_NAME}"을,--certificate-oidc-issuer에"${CI_SERVER_URL}"을 사용할 수 있어요.
빌드 아티팩트
다음 예시는 GitLab CI에서 서명된 빌드 아티팩트를 검증하는 방법을 보여줘요. 아티팩트를 검증하려면 아티팩트 자체와 cosign sign-blob이 만들어낸 cosign.bundle 파일이 모두 필요해요. 앞서 설명한 명령줄 인자를 사용해요.
verify_artifact:
stage: verify
image: alpine:latest
before_script:
- apk add --update cosign
script:
- cosign verify-blob artifact.txt --bundle cosign.bundle --certificate-identity "https://gitlab.com/my-group/my-project//path/to/.gitlab-ci.yml@refs/heads/main" --certificate-oidc-issuer "https://gitlab.com"
추가 세부 사항:
- 프로젝트 경로와
.gitlab-ci.yml경로 사이의 이중 슬래시는 오타가 아니라 검증이 성공하려면 반드시 필요한 부분이에요. 단일 슬래시를 쓰면 흔히 나는 오류는Error: none of the expected identities matched what was in the certificate, got subjects인데, 그 뒤에 프로젝트 경로와.gitlab-ci.yml경로 사이에 슬래시가 두 개 있는 서명 URL이 표시돼요. - 검증이 서명과 같은 파이프라인에서 일어난다면
--certificate-identity에"${CI_PROJECT_URL}//.gitlab-ci.yml@refs/heads/${CI_COMMIT_REF_NAME}"을,--certificate-oidc-issuer에"${CI_SERVER_URL}"을 사용할 수 있어요.
Sigstore와 npm으로 키리스 provenance 생성하기
Sigstore와 npm을 GitLab CI/CD와 함께 사용하면 키 관리 오버헤드 없이 빌드 아티팩트를 디지털 서명할 수 있어요.
npm provenance에 대하여
npm CLI는 패키지 관리자가 사용자에게 provenance(출처 증명) 증명을 제공할 수 있게 해줘요. npm CLI provenance 생성 기능을 쓰면 사용자가 다운로드해 사용하는 패키지가 정말 여러분과 그 빌드 시스템에서 만든 것임을 신뢰하고 검증할 수 있어요.
npm 패키지 게시에 대한 자세한 내용은 GitLab npm 패키지 레지스트리를 참고해요.
Sigstore
Sigstore는 패키지 관리자와 보안 전문가가 소프트웨어 공급망을 공격으로부터 보호하는 데 쓸 수 있는 도구 모음이에요. Fulcio, Cosign, Rekor 같은 무료 오픈 소스 기술을 모아서, 오픈 소스 소프트웨어를 배포·사용할 때 더 안전하게 해주는 데 필요한 디지털 서명, 검증, provenance 검사를 처리해요.
관련 주제:
GitLab CI/CD에서 provenance 생성하기
앞서 설명했듯이 Sigstore가 GitLab OIDC를 지원하므로, npm provenance를 GitLab CI/CD·Sigstore와 함께 사용해서 GitLab CI/CD 파이프라인에서 npm 패키지의 provenance를 생성하고 서명할 수 있어요.
사전 준비
- GitLab ID 토큰의
aud를sigstore로 설정해요. - npm publish에
--provenance플래그를 추가해요.
.gitlab-ci.yml 파일에 추가할 예시 내용:
build:
image: node:latest
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- npm publish --provenance --access public
npm GitLab 템플릿도 이 기능을 제공해요. 예시는 템플릿 문서에 있어요.
npm provenance 검증하기
npm CLI는 최종 사용자가 패키지의 provenance를 검증하는 기능도 제공해요.
npm audit signatures
audited 1 package in 0s
1 package has a verified registry signature
provenance 메타데이터 살펴보기
Rekor 투명성 로그는 provenance와 함께 게시되는 모든 패키지의 인증서와 증명을 저장해요. 예를 들어 이 예시의 항목이 있어요.
npm이 생성한 provenance 문서 예시:
_type: https://in-toto.io/Statement/v0.1
subject:
- name: pkg:npm/%40strongjz/[email protected]
digest:
sha512: >-
924a134a0fd4fe6a7c87b4687bf0ac898b9153218ce9ad75798cc27ab2cddbeff77541f3847049bd5e3dfd74cea0a83754e7686852f34b185c3621d3932bc3c8
predicateType: https://slsa.dev/provenance/v0.2
predicate:
buildType: https://github.com/npm/CLI/gitlab/v0alpha1
builder:
id: https://gitlab.com/strongjz/npm-provenance-example/-/runners/12270835
invocation:
configSource:
uri: git+https://gitlab.com/strongjz/npm-provenance-example
digest:
sha1: 6e02e901e936bfac3d4691984dff8c505410cbc3
entryPoint: deploy
parameters:
CI: 'true'
CI_API_GRAPHQL_URL: https://gitlab.com/api/graphql
CI_API_V4_URL: https://gitlab.com/api/v4
CI_COMMIT_BEFORE_SHA: 7d3e913e5375f68700e0c34aa90b0be7843edf6c
CI_COMMIT_BRANCH: main
CI_COMMIT_REF_NAME: main
CI_COMMIT_REF_PROTECTED: 'true'
CI_COMMIT_REF_SLUG: main
CI_COMMIT_SHA: 6e02e901e936bfac3d4691984dff8c505410cbc3
CI_COMMIT_SHORT_SHA: 6e02e901
CI_COMMIT_TIMESTAMP: '2023-05-19T10:17:12-04:00'
CI_COMMIT_TITLE: trying to publish to gitlab reg
CI_CONFIG_PATH: .gitlab-ci.yml
CI_DEFAULT_BRANCH: main
CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX: gitlab.com:443/strongjz/dependency_proxy/containers
CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX: gitlab.com:443/strongjz/dependency_proxy/containers
CI_DEPENDENCY_PROXY_SERVER: gitlab.com:443
CI_DEPENDENCY_PROXY_USER: gitlab-ci-token
CI_JOB_ID: '4316132595'
CI_JOB_NAME: deploy
CI_JOB_NAME_SLUG: deploy
CI_JOB_STAGE: deploy
CI_JOB_STARTED_AT: '2023-05-19T14:17:23Z'
CI_JOB_URL: https://gitlab.com/strongjz/npm-provenance-example/-/jobs/4316132595
CI_NODE_TOTAL: '1'
CI_PAGES_DOMAIN: gitlab.io
CI_PAGES_URL: https://strongjz.gitlab.io/npm-provenance-example
CI_PIPELINE_CREATED_AT: '2023-05-19T14:17:21Z'
CI_PIPELINE_ID: '872773336'
CI_PIPELINE_IID: '40'
CI_PIPELINE_SOURCE: push
CI_PIPELINE_URL: https://gitlab.com/strongjz/npm-provenance-example/-/pipelines/872773336
CI_PROJECT_CLASSIFICATION_LABEL: ''
CI_PROJECT_DESCRIPTION: ''
CI_PROJECT_ID: '45821955'
CI_PROJECT_NAME: npm-provenance-example
CI_PROJECT_NAMESPACE: strongjz
CI_PROJECT_NAMESPACE_SLUG: strongjz
CI_PROJECT_NAMESPACE_ID: '36018'
CI_PROJECT_PATH: strongjz/npm-provenance-example
CI_PROJECT_PATH_SLUG: strongjz-npm-provenance-example
CI_PROJECT_REPOSITORY_LANGUAGES: javascript,dockerfile
CI_PROJECT_ROOT_NAMESPACE: strongjz
CI_PROJECT_TITLE: npm-provenance-example
CI_PROJECT_URL: https://gitlab.com/strongjz/npm-provenance-example
CI_PROJECT_VISIBILITY: public
CI_REGISTRY: registry.gitlab.com
CI_REGISTRY_IMAGE: registry.gitlab.com/strongjz/npm-provenance-example
CI_REGISTRY_USER: gitlab-ci-token
CI_RUNNER_DESCRIPTION: 3-blue.shared.runners-manager.gitlab.com/default
CI_RUNNER_ID: '12270835'
CI_RUNNER_TAGS: >-
["gce", "east-c", "linux", "ruby", "mysql", "postgres", "mongo",
"git-annex", "shared", "docker", "saas-linux-small-amd64"]
CI_SERVER_HOST: gitlab.com
CI_SERVER_NAME: GitLab
CI_SERVER_PORT: '443'
CI_SERVER_PROTOCOL: https
CI_SERVER_REVISION: 9d4873fd3c5
CI_SERVER_SHELL_SSH_HOST: gitlab.com
CI_SERVER_SHELL_SSH_PORT: '22'
CI_SERVER_URL: https://gitlab.com
CI_SERVER_VERSION: 16.1.0-pre
CI_SERVER_VERSION_MAJOR: '16'
CI_SERVER_VERSION_MINOR: '1'
CI_SERVER_VERSION_PATCH: '0'
CI_TEMPLATE_REGISTRY_HOST: registry.gitlab.com
GITLAB_CI: 'true'
GITLAB_FEATURES: >-
elastic_search,ldap_group_sync,multiple_ldap_servers,seat_link,usage_quotas,zoekt_code_search,repository_size_limit,admin_audit_log,auditor_user,custom_file_templates,custom_project_templates,db_load_balancing,default_branch_protection_restriction_in_groups,extended_audit_events,external_authorization_service_api_management,geo,instance_level_scim,ldap_group_sync_filter,object_storage,pages_size_limit,project_aliases,password_complexity,enterprise_templates,git_abuse_rate_limit,required_ci_templates,runner_maintenance_note,runner_performance_insights,runner_upgrade_management,runner_jobs_statistics
GITLAB_USER_ID: '31705'
GITLAB_USER_LOGIN: strongjz
environment:
name: 3-blue.shared.runners-manager.gitlab.com/default
architecture: linux/amd64
server: https://gitlab.com
project: strongjz/npm-provenance-example
job:
id: '4316132595'
pipeline:
id: '872773336'
ref: .gitlab-ci.yml
metadata:
buildInvocationId: https://gitlab.com/strongjz/npm-provenance-example/-/jobs/4316132595
completeness:
parameters: true
environment: true
materials: false
reproducible: false
materials:
- uri: git+https://gitlab.com/strongjz/npm-provenance-example
digest:
sha1: 6e02e901e936bfac3d4691984dff8c505410cbc3
더 알아보기
키리스 서명의 개념을 더 깊이 이해하려면 Sigstore의 Cosign 퀵스타트와 ID 토큰 인증 문서를 함께 살펴보면 좋아요. 자체 호스팅 Sigstore 인프라를 쓰는 환경이라면 자가 호스팅 Sigstore 서명 문서를 확인해 보세요.