자체 호스팅 Sigstore로 아티팩트와 컨테이너 이미지 서명하기

자체 호스팅 Sigstore로 아티팩트와 컨테이너 이미지 서명하기

  • Tier: Free, Premium, Ultimate
  • Offering: GitLab Self-Managed

GitLab Self-Managed를 운영 중이라면 공개 Sigstore 서비스를 사용해 CI/CD 아티팩트와 컨테이너 이미지를 서명할 수 없어요. 그 서비스는 GitLab.com 파이프라인만 신뢰하고 내 인스턴스의 파이프라인은 신뢰하지 않기 때문이에요. 대신 내 GitLab 인스턴스에 자체 Sigstore 인프라(Fulcio, Rekor, Certificate Transparency 로그)를 연결해서, GitLab.com이나 인터넷에 의존하지 않고 Cosign으로 아티팩트를 서명·검증할 수 있어요.

GitLab OpenID Connect(OIDC) 제공자가 내 신원을 증명하고, Cosign은 서명 키를 사용 직후 바로 버려요. Rekor는 모든 서명 이벤트를 투명성 로그에 기록하죠.

Fulcio가 발급한 인증서는 어떤 파이프라인이 만들었는지에 대한 세부 정보(프로젝트 경로, 커밋 SHA, 파이프라인 소스, 러너 환경, 잡 URL)와 함께 서명에 내장돼요.

사전 준비 사항은 다음과 같아요.

  • Sigstore 인프라(Fulcio, Rekor)와 GitLab CI/CD 러너를 설정할 수 있는 권한.
  • 네트워크 안에서 Fulcio, Rekor, Certificate Transparency 로그를 포함한 자체 호스팅 Sigstore 스택이 실행 중이어야 함. Certificate Transparency 로그는 필수예요. Cosign은 검증할 때 서명된 인증서 타임스탬프를 확인하므로, Certificate Transparency 로그 없이 배포된 Fulcio는 Cosign이 검증할 수 없는 인증서를 발급합니다. 배포 방법은 다음을 참고하세요.
  • 내 GitLab 인스턴스가 사설 인증 기관으로 HTTPS를 사용한다면, 그 인증 기관이 Fulcio와 CI/CD 러너 양쪽에서 신뢰되어야 해요.
    • Fulcio 컨테이너에 인증 기관 인증서를 마운트하고 SSL_CERT_FILE을 그 경로로 설정.
    • 각 러너의 운영체제 신뢰 저장소에 인증 기관 인증서를 설치.
    • CI/CD 러너에 Cosign v2.x 이상 설치.
    • Sigstore 스택의 트러스트 자료 파일들을 러너의 공유 위치(예: /etc/sigstore/)에 배치.
      • Fulcio 루트 CA 인증서(fulcio-root.pem)
      • Rekor 투명성 로그 공개 키(rekor-pub.pem)
      • Certificate Transparency 로그 공개 키(ctfe-pub.pem)

출처: 문서

본문

GitLab 인스턴스를 신뢰하도록 Fulcio 구성하기

키리스(keyless) 서명 중에 GitLab CI/CD 잡의 OIDC 토큰을 검증할 수 있도록, Fulcio가 내 GitLab 인스턴스를 신뢰하게 구성하세요. Fulcio는 토큰의 클레임을 서명 인증서의 필드에 매핑해요. 설정 파일에는 oidc-issuers 섹션과 ci-issuer-metadata 섹션이 모두 필요해요.

Fulcio를 구성하려면:

  • 다음 명령으로 정확한 OIDC 발급자 URL을 가져온다. https://gitlab.example.com을 내 GitLab 인스턴스의 URL로 바꾸세요. Fulcio는 정확한 일치를 요구해요. 스킴과 마지막 슬래시의 유무 모두 중요해요.
curl --silent "https://gitlab.example.com/.well-known/openid-configuration" | jq --raw-output .issuer
  • 내 GitLab 인스턴스의 oidc-issuers 항목이 들어 있는 Fulcio OIDC 설정 파일을 만든다. <gitlab_issuer_url>을 이전 단계의 출력으로 바꾸세요.
oidc-issuers:
  <gitlab_issuer_url>:
    issuer-url: <gitlab_issuer_url>
    client-id: sigstore
    type: ci-provider
    ci-provider: gitlab-pipeline
    contact: [email protected]
    description: "GitLab Self-Managed OIDC"
  • 같은 파일에 업스트림 Fulcio 설정에서 복사한 GitLab 클레임 템플릿이 들어 있는 ci-issuer-metadata 섹션을 추가한다. <gitlab_issuer_url>은 이전 단계에서 사용한 것과 같은 값으로 바꾸세요.

각 클레임이 무엇에 매핑되는지는 OIDC 토큰 클레임을 Fulcio OID에 매핑 표의 GitLab 열을 참고하세요.

ci-issuer-metadata:
  gitlab-pipeline:
    default-template-values:
      url: "<gitlab_issuer_url>"
      environment: ""
    extension-templates:
      build-signer-uri: "https://{{ .ci_config_ref_uri }}"
      build-signer-digest: "ci_config_sha"
      runner-environment: "runner_environment"
      source-repository-uri: "{{ .url }}/{{ .project_path }}"
      source-repository-digest: "sha"
      source-repository-ref: >-
        refs/{{if eq .ref_type "branch"}}heads/{{ else }}tags/{{end}}{{ .ref }}
      source-repository-identifier: "project_id"
      source-repository-owner-uri: "{{ .url }}/{{ .namespace_path }}"
      source-repository-owner-identifier: "namespace_id"
      build-config-uri: "https://{{ .ci_config_ref_uri }}"
      build-config-digest: "ci_config_sha"
      build-trigger: "pipeline_source"
      run-invocation-uri: >-
        {{ .url }}/{{ .project_path }}/-/jobs/{{ .job_id }}
      source-repository-visibility-at-signing: "project_visibility"
      deployment-environment: "environment"
    subject-alternative-name-template: "https://{{ .ci_config_ref_uri }}"

이 섹션은 GitLab이 아니라 Sigstore 프로젝트가 관리해요. 업스트림 파일의 변경 사항을 주기적으로 확인하고 내 복사본도 그에 맞게 갱신하세요.

아티팩트와 컨테이너 이미지 서명하기

id_tokens 키워드로 잡용 OIDC 토큰을 생성하세요. Cosign이 이 토큰을 Fulcio에 제시하고, Fulcio는 일회용 서명 인증서를 발급해요.

Cosign v3는 서명 설정 파일을 사용해 Sigstore 서비스 엔드포인트와 트러스트 자료를 지정해요. 이 파일들은 한 번 만들어서 러너에 배포하거나, 아래 예시처럼 각 잡에서 생성할 수 있어요.

sign-artifact:
  stage: sign
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  variables:
    COSIGN_YES: "true"
  script:
    - cosign signing-config create
        --fulcio="url=http://<sigstore-host>:5555,api-version=1,start-time=2024-01-01T00:00:00Z,operator=my-org"
        --rekor="url=http://<sigstore-host>:3000,api-version=1,start-time=2024-01-01T00:00:00Z,operator=my-org"
        --rekor-config="ANY"
        --oidc-provider="url=https://gitlab.example.com,api-version=1,start-time=2024-01-01T00:00:00Z,operator=my-org"
        --out signing-config.json
    - cosign trusted-root create
        --fulcio="url=http://<sigstore-host>:5555,certificate-chain=/etc/sigstore/fulcio-root.pem,start-time=2024-01-01T00:00:00Z"
        --rekor="url=http://<sigstore-host>:3000,public-key=/etc/sigstore/rekor-pub.pem,start-time=2024-01-01T00:00:00Z"
        --ctfe="url=http://<sigstore-host>:6962,public-key=/etc/sigstore/ctfe-pub.pem,start-time=2024-01-01T00:00:00Z"
        --out trusted-root.json
    - cosign sign-blob
        --signing-config=signing-config.json
        --trusted-root=trusted-root.json
        --oidc-client-id=sigstore
        --identity-token=$SIGSTORE_ID_TOKEN
        --bundle=artifact.bundle
        artifact.txt

start-time 값은 Sigstore protobuf 스펙에 정의된 대로 서비스 엔드포인트가 유효한 것으로 간주되는 가장 빠른 시각이에요. Sigstore 스택이 배포된 날짜를 사용하세요.

--oidc-client-id의 sigstore를 Fulcio OIDC 발급자 항목에 설정한 client ID로 바꾸세요.

--oidc-provider에는 Fulcio의 oidc-issuers 설정에 등록한 것과 같은 발급자 URL을 사용하세요.

컨테이너 이미지 서명하기

컨테이너 이미지를 서명하려면 파일 경로 대신 이미지 참조와 함께 cosign sign을 사용하세요. 서명 설정은 동일해요.

컨테이너 레지스트리는 해석 가능한 호스트 이름으로 HTTPS를 통해 접근 가능해야 해요. 레지스트리는 자체 외부 URL에서 인증 영역(realm)을 알려요. Cosign은 호스트가 글자 그대로의 사설·링크-로컬 주소인 영역을 거부해요. registry_external_url이 IP 주소 그대로인 인스턴스는 파일은 서명할 수 있지만 컨테이너 이미지는 서명할 수 없어요.

Cosign v2.x 사용 시

Cosign v2.x를 쓴다면 설정 파일 대신 URL 플래그와 환경 변수를 사용하세요.

sign-artifact:
  stage: sign
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  variables:
    COSIGN_YES: "true"
    SIGSTORE_ROOT_FILE: /etc/sigstore/fulcio-root.pem
    SIGSTORE_REKOR_PUBLIC_KEY: /etc/sigstore/rekor-pub.pem
    SIGSTORE_CT_LOG_PUBLIC_KEY_FILE: /etc/sigstore/ctfe-pub.pem
  script:
    - cosign sign-blob
        --fulcio-url=http://<sigstore-host>:5555
        --rekor-url=http://<sigstore-host>:3000
        --oidc-issuer=https://gitlab.example.com
        --identity-token=$SIGSTORE_ID_TOKEN
        --output-signature=artifact.sig
        --output-certificate=artifact.crt
        artifact.txt

컨테이너 이미지는 컨테이너 이미지 서명에 설명된 대로 Cosign v3를 사용하세요.

Cosign v2는 인증서를 base64로 인코딩된 PEM으로 써요. 다른 도구에 넘기기 전에 디코딩하세요.

--oidc-issuer에는 Fulcio의 oidc-issuers 설정에 등록한 것과 같은 발급자 URL을 사용하세요.

서명 검증하기

Cosign v3 번들로 검증할 때는 트러스트 자료와 예상 서명자 신원을 사용해요.

cosign verify-blob \
  --trusted-root=trusted-root.json \
  --bundle=artifact.bundle \
  --certificate-oidc-issuer=https://gitlab.example.com \
  --certificate-identity=https://gitlab.example.com/my-group/my-project//.gitlab-ci.yml@refs/heads/main \
  artifact.txt

--certificate-identity 값은 서명 인증서의 Subject Alternative Name이고, CI/CD 설정 경로에서 만들어져요.

https://<CI_SERVER_HOST>/<CI_PROJECT_PATH>//.gitlab-ci.yml@<full ref>

이 패턴에서 조심할 점이 세 가지 있어요.

  • 스킴은 항상 https://예요. Fulcio 설정의 subject-alternative-name-template가 그렇게 정하기 때문이죠. 인스턴스가 HTTP로 접근 가능하더라도 마찬가지예요.
  • .gitlab-ci.yml 앞의 이중 슬래시는 올바른 표기예요. 그 앞의 경로 조각은 프로젝트이고, 뒤에 오는 빈 조각은 기본 CI/CD 설정 위치를 담아요.
  • full ref는 브랜치 파이프라인이면 refs/heads/, 태그 파이프라인이면 refs/tags/예요.

Cosign v2로 서명한 아티팩트를 검증하려면 분리된(detached) 서명과 인증서를 전달하세요.

cosign verify-blob \
  --signature=artifact.sig \
  --certificate=artifact.crt \
  --certificate-oidc-issuer=https://gitlab.example.com \
  --certificate-identity=https://gitlab.example.com/my-group/my-project//.gitlab-ci.yml@refs/heads/main \
  artifact.txt

관련 주제

문제 해결

자체 호스팅 Sigstore 인프라로 아티팩트를 서명할 때 다음 문제들을 만날 수 있어요.

Error: metadata not found for ci provider gitlab-pipeline 오류

이 오류는 Fulcio 설정에 ci-issuer-metadata 섹션이 없을 때 발생해요.

해결하려면 GitLab 인스턴스를 신뢰하도록 Fulcio 구성에 문서화된 대로 전체 ci-issuer-metadata 블록을 추가하세요.

Error: ctfe public key not found for payload 오류

이 오류는 Cosign이 자체 호스팅 스택의 Certificate Transparency 로그 공개 키를 찾을 수 없을 때 발생해요.

해결하려면 Cosign v3에서는 cosign trusted-root create 명령에 --ctfe를 포함하고, Cosign v2에서는 SIGSTORE_CT_LOG_PUBLIC_KEY_FILE 환경 변수를 설정하세요.

Error: not enough verified log entries from transparency log 오류

이 오류는 trusted-root.json의 Rekor 공개 키가 Rekor 인스턴스가 사용하는 키와 더 이상 일치하지 않을 때 발생해요. 메모리 기반 서명자는 재시작할 때마다 새 키와 새 빈 머클 트리(Rekor의 위변조 방지 로그 구조)를 생성해요. 따라서 재시작할 때마다 트러스트 자료가 무효화되고, 다시 만들어질 때까지 모든 잡의 서명이 실패해요.

해결하려면 실행 중인 인스턴스에서 현재 키를 읽어 트러스트 루트를 다시 생성하세요.

curl --silent --fail "http://<sigstore-host>:3000/api/v1/log/publicKey" --output /etc/sigstore/rekor-pub.pem
cosign trusted-root create \
  --fulcio="url=http://<sigstore-host>:5555,certificate-chain=/etc/sigstore/fulcio-root.pem,start-time=2024-01-01T00:00:00Z" \
  --rekor="url=http://<sigstore-host>:3000,public-key=/etc/sigstore/rekor-pub.pem,start-time=2024-01-01T00:00:00Z" \
  --ctfe="url=http://<sigstore-host>:6962,public-key=/etc/sigstore/ctfe-pub.pem,start-time=2024-01-01T00:00:00Z" \
  --out trusted-root.json

재생성된 trusted-root.json을 모든 러너에 배포하세요.

이 문제를 근본적으로 막으려면 Rekor를 영구 서명 키와 고정 트리 식별자로 구성하세요.

Error: failed to verify signed certificate timestamp 오류

이 오류는 인증서에 서명된 인증서 타임스탬프가 없을 때 발생하는데, Fulcio가 Certificate Transparency 로그 없이 실행될 때 그렇게 돼요. 이 구성에서는 서명은 성공하고 검증만 실패해요.

해결하려면 Fulcio를 트러스트 루트를 만들 때 --ct-log-url이 Certificate Transparency 로그를 가리키게 설정하고 --ctfe를 포함하세요.

Error: x509: certificate signed by unknown authority 오류

이 오류는 Fulcio가 내 GitLab 인스턴스가 제시하는 TLS 인증서를 검증할 수 없을 때 발생해요.

해결하려면 사설 인증 기관 인증서를 Fulcio 컨테이너에 마운트하고 SSL_CERT_FILE을 그 경로로 설정하세요.

Fulcio의 /healthz 엔드포인트는 OIDC 제공자 초기화가 실패해도 SERVING을 보고해요. 컨테이너 로그로 제공자가 실제로 로드됐는지 확인하세요.

더 알아보기

키리스 서명의 기반이 되는 OIDC ID 토큰 설정이 궁금하다면 CI/CD OIDC ID 토큰 문서를 먼저 읽어 보세요. GitLab.com에서 공개 Sigstore를 사용하는 방식과 비교하고 싶다면 서명 예시 문서가 좋은 참고가 됩니다.