튜토리얼: GitLab CI/CD로 Python 패키지 빌드 및 서명
이 튜토리얼은 Python 패키지를 위한 안전한 파이프라인을 구현하는 방법을 보여줍니다. 이 튜토리얼을 완료하면 다음을 배울 수 있습니다: 패키지 서명은 몇 가지 중요한 보안 이점을 제공합니다: 이 튜토리얼을 완료하려면 다음이 필요합니다:
이 튜토리얼은 Python 패키지를 위한 안전한 파이프라인을 구현하는 방법을 보여줍니다. 파이프라인에는 GitLab CI/CD와 Sigstore Cosign을 사용하여 Python 패키지를 암호화적으로 서명하고 검증하는 단계가 포함됩니다.
이 튜토리얼을 완료하면 다음을 배울 수 있습니다:
- GitLab CI/CD를 사용하여 Python 패키지를 빌드하고 서명하는 방법.
- 일반 패키지 레지스트리를 사용하여 패키지 서명을 저장하고 관리하는 방법.
- 최종 사용자로서 패키지 서명을 검증하는 방법.
패키지 서명의 이점은 무엇인가요?#
패키지 서명은 몇 가지 중요한 보안 이점을 제공합니다:
- 진위성: 사용자는 패키지가 신뢰할 수 있는 소스에서 왔는지 검증할 수 있습니다.
- 데이터 무결성: 패키지가 배포 중에 변조된 경우 감지됩니다.
- 부인 방지: 패키지의 출처를 암호화적으로 증명할 수 있습니다.
- 공급망 보안: 패키지 서명은 공급망 공격 및 손상된 저장소로부터 보호합니다.
시작하기 전에#
이 튜토리얼을 완료하려면 다음이 필요합니다:
- GitLab 계정 및 테스트 프로젝트.
- Python 패키지, GitLab CI/CD 및 패키지 레지스트리 개념에 대한 기본 지식.
단계#
다음은 수행할 작업의 개요입니다:
- Python 프로젝트 설정.
- 기본 구성 추가.
- 빌드 단계 구성.
- 서명 단계 구성.
- 검증 단계 구성.
- 게시 단계 구성.
- 서명 게시 단계 구성.
- 소비자 검증 단계 구성.
- 사용자로서 패키지 검증.
Python 프로젝트 설정#
먼저 테스트 프로젝트를 만듭니다. 프로젝트 루트에 pyproject.toml 파일을 추가합니다:
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "<my_package>" # Will be dynamically replaced by CI/CD pipeline
version = "<1.0.0>" # Will be dynamically replaced by CI/CD pipeline
description = ""
readme = "README.md"
requires-python = ">=3.7"
authors = [
{name = "", email = "<your.email@example.com>"},
]
[project.urls]
"Homepage" = "<https://gitlab.com/my_package>" # Will be replaced with actual project URL
Your Name과 your.email@example.com을 자신의 개인 정보로 바꿔야 합니다.
다음 단계에서 CI/CD 파이프라인 빌드를 완료하면 파이프라인이 자동으로 다음을 수행합니다:
my_package를 프로젝트 이름의 정규화된 버전으로 교체합니다.version을 파이프라인 버전과 일치하도록 변경합니다.HomepageURL을 GitLab 프로젝트 URL과 일치하도록 변경합니다.
기본 구성 추가#
프로젝트 루트에 .gitlab-ci.yml 파일을 추가합니다. 다음 구성을 추가합니다:
variables:
# Base Python version for all jobs
PYTHON_VERSION: '3.10'
# Package names and versions
PACKAGE_NAME: ${CI_PROJECT_NAME}
PACKAGE_VERSION: "1.0.0" # Use semantic versioning
# Sigstore service URLs
FULCIO_URL: 'https://fulcio.sigstore.dev'
REKOR_URL: 'https://rekor.sigstore.dev'
# Identity for Sigstore verification
CERTIFICATE_IDENTITY: 'https://gitlab.com/${CI_PROJECT_PATH}//.gitlab-ci.yml@refs/heads/${CI_DEFAULT_BRANCH}'
CERTIFICATE_OIDC_ISSUER: 'https://gitlab.com'
# Pip cache directory for faster builds
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
# Auto-accept prompts from Cosign
COSIGN_YES: "true"
# Base URL for generic package registry
GENERIC_PACKAGE_BASE_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}"
default:
before_script:
# Normalize package name once at the start of any job
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# Template for Python-based jobs
.python-job:
image: python:${PYTHON_VERSION}
before_script:
# First normalize package name
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# Then install Python dependencies
- pip install --upgrade pip
- pip install build twine setuptools wheel
cache:
paths:
- ${PIP_CACHE_DIR}
# Template for Python + Cosign jobs
.python+cosign-job:
extends: .python-job
before_script:
# First normalize package name
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# Then install dependencies
- apt-get update && apt-get install -y curl wget
- wget -O cosign https://github.com/sigstore/cosign/releases/download/v2.2.3/cosign-linux-amd64
- chmod +x cosign && mv cosign /usr/local/bin/
- export COSIGN_EXPERIMENTAL=1
- pip install --upgrade pip
- pip install build twine setuptools wheel
stages:
- build
- sign
- verify
- publish
- publish_signatures
- consumer_verification
이 기본 구성은:
- 일관성을 위해 Python
3.10을 기본 이미지로 사용하도록 파이프라인에 지시합니다 - 기본 Python 작업을 위한
.python-job과 서명 작업을 위한.python+cosign-job두 가지 재사용 가능한 템플릿을 설정합니다 - 빌드 속도를 높이기 위해 pip 캐싱을 구현합니다
- Python 호환성을 위해 하이픈을 언더스코어로 변환하여 패키지 이름을 정규화합니다
- 쉬운 관리를 위해 모든 주요 변수를 파이프라인 수준에서 정의합니다
빌드 단계 구성#
빌드 단계는 Python 배포 패키지를 빌드합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
build:
extends: .python-job
stage: build
script:
# Initialize git repo with actual content
- git init
- git config --global init.defaultBranch main
- git config --global user.email "ci@example.com"
- git config --global user.name "CI"
- git add .
- git commit -m "Initial commit"
# Update package name, version, and homepage URL in pyproject.toml
- sed -i "s/name = \".*\"/name = \"${NORMALIZED_NAME}\"/" pyproject.toml
- sed -i "s/version = \".*\"/version = \"${PACKAGE_VERSION}\"/" pyproject.toml
- sed -i "s|\"Homepage\" = \".*\"|\"Homepage\" = \"https://gitlab.com/${CI_PROJECT_PATH}\"|" pyproject.toml
# Debug: show updated file
- echo "Updated pyproject.toml contents:"
- cat pyproject.toml
# Build package
- python -m build
artifacts:
paths:
- dist/
- pyproject.toml
빌드 단계 구성은:
- 빌드 컨텍스트를 위한 Git 저장소를 초기화합니다
pyproject.toml의 패키지 메타데이터를 동적으로 업데이트합니다- 휠(
.whl)과 소스 배포(.tar.gz) 패키지를 모두 추가합니다 - 이후 단계를 위해 빌드 아티팩트를 보존합니다
- 문제 해결을 위한 디버그 출력을 제공합니다
서명 단계 구성#
서명 단계는 Sigstore Cosign을 사용하여 패키지에 서명합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
sign:
extends: .python+cosign-job
stage: sign
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- |
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
cosign sign-blob --yes \
--fulcio-url=${FULCIO_URL} \
--rekor-url=${REKOR_URL} \
--oidc-issuer $CI_SERVER_URL \
--identity-token $SIGSTORE_ID_TOKEN \
--output-signature "dist/${filename}.sig" \
--output-certificate "dist/${filename}.crt" \
"$file"
# Debug: Verify files were created
echo "Checking generated signature and certificate:"
ls -l "dist/${filename}.sig" "dist/${filename}.crt"
fi
done
artifacts:
paths:
- dist/
서명 단계 구성은:
- 향상된 보안을 위해 Sigstore의 키 없는 서명을 사용합니다
- 휠과 소스 배포 패키지 모두에 서명합니다
- 별도의 서명(
.sig)과 인증서(.crt) 파일을 생성합니다 - 인증을 위해 OIDC 통합을 사용합니다
- 서명 생성을 위한 상세 로깅을 포함합니다
검증 단계 구성#
검증 단계는 서명을 로컬에서 검증합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
verify:
extends: .python+cosign-job
stage: verify
script:
- |
failed=0
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
echo "Verifying file: $file"
echo "Using signature: dist/${filename}.sig"
echo "Using certificate: dist/${filename}.crt"
if ! cosign verify-blob \
--signature "dist/${filename}.sig" \
--certificate "dist/${filename}.crt" \
--certificate-identity "${CERTIFICATE_IDENTITY}" \
--certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \
"$file"; then
echo "Verification failed for $filename"
failed=1
fi
fi
done
if [ $failed -eq 1 ]; then
exit 1
fi
검증 단계 구성은:
- 서명 직후 서명을 검증합니다
- 휠과 소스 배포 패키지를 모두 확인합니다
- 인증서 ID와 OIDC 발급자를 검증합니다
- 검증이 실패하면 빠르게 실패합니다
- 상세 검증 로그를 제공합니다
게시 단계 구성#
게시 단계는 GitLab PyPI 패키지 레지스트리에 패키지를 업로드합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
publish:
extends: .python-job
stage: publish
script:
- |
# Configure PyPI settings for GitLab package registry
cat << EOF > ~/.pypirc
[distutils]
index-servers = gitlab
[gitlab]
repository = ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
username = gitlab-ci-token
password = ${CI_JOB_TOKEN}
EOF
# Upload packages using twine
TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token \
twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi \
dist/*.whl dist/*.tar.gz
게시 단계 구성은:
- PyPI 레지스트리 인증을 구성합니다
- GitLab 내장 패키지 레지스트리를 사용합니다
- 휠과 소스 배포를 모두 게시합니다
- 안전한 인증을 위해 작업 토큰을 사용합니다
- 재사용 가능한
.pypirc구성을 생성합니다
서명 게시 단계 구성#
서명 게시 단계는 GitLab 일반 패키지 레지스트리에 서명을 저장합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
publish_signatures:
extends: .python+cosign-job
stage: publish_signatures
script:
- |
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
ls -l "dist/${filename}.sig" "dist/${filename}.crt"
echo "Publishing signatures for $filename"
echo "Publishing to: ${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
# Upload signature and certificate
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--fail \
--upload-file "dist/${filename}.sig" \
"${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--fail \
--upload-file "dist/${filename}.crt" \
"${GENERIC_PACKAGE_BASE_URL}/${filename}.crt"
fi
done
서명 게시 단계 구성은:
- 일반 패키지 레지스트리에 서명을 저장합니다
- 서명-패키지 매핑을 유지합니다
- 아티팩트에 일관된 명명 규칙을 사용합니다
- 서명의 크기 검증을 포함합니다
- 상세 업로드 로그를 제공합니다
소비자 검증 단계 구성#
소비자 검증 단계는 최종 사용자 패키지 검증을 시뮬레이션합니다.
.gitlab-ci.yml 파일에 다음 구성을 추가합니다:
consumer_verification:
extends: .python+cosign-job
stage: consumer_verification
script:
- |
# Initialize git repo for setuptools_scm
git init
git config --global init.defaultBranch main
# Create directory for downloading packages
mkdir -p pkg signatures
# Download the specific wheel version
pip download --index-url "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple" \
"${NORMALIZED_NAME}==${PACKAGE_VERSION}" --no-deps -d ./pkg --verbose
# Download the specific source distribution version
pip download --no-binary :all: \
--index-url "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple" \
"${NORMALIZED_NAME}==${PACKAGE_VERSION}" --no-deps -d ./pkg --verbose
failed=0
for file in pkg/*.whl pkg/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
sig_url="${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
cert_url="${GENERIC_PACKAGE_BASE_URL}/${filename}.crt"
echo "Downloading signatures for $filename"
echo "Signature URL: $sig_url"
echo "Certificate URL: $cert_url"
# Download signatures
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output "signatures/${filename}.sig" \
"$sig_url"
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output "signatures/${filename}.crt" \
"$cert_url"
# Verify signature
if ! cosign verify-blob \
--signature "signatures/${filename}.sig" \
--certificate "signatures/${filename}.crt" \
--certificate-identity "${CERTIFICATE_IDENTITY}" \
--certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \
"$file"; then
echo "Signature verification failed"
failed=1
fi
fi
done
if [ $failed -eq 1 ]; then
echo "Verification failed for one or more packages"
exit 1
fi
소비자 검증 단계 구성은:
- 실제 패키지 설치를 시뮬레이션합니다
- 두 가지 패키지 형식을 모두 다운로드하고 검증합니다
- 일관성을 위해 정확한 버전 매칭을 사용합니다
- 포괄적인 오류 처리를 구현합니다
- 전체 검증 워크플로우를 테스트합니다
사용자로서 패키지 검증#
최종 사용자로서 다음 단계를 통해 패키지 서명을 검증할 수 있습니다:
-
Cosign 설치:
wget -O cosign https://github.com/sigstore/cosign/releases/download/v2.2.3/cosign-linux-amd64 chmod +x cosign && sudo mv cosign /usr/local/bin/Cosign은 전역 설치를 위한 특별 권한이 필요합니다. 권한 문제를 우회하려면
sudo를 사용하세요. -
패키지와 서명 다운로드:
# You can find your PROJECT_ID in your GitLab project's home page under the project name # Download the specific version of the package pip download your-package-name==1.0.0 --no-deps # The FILENAME will be the output from the pip download command # For example: your-package-name-1.0.0.tar.gz or your-package-name-1.0.0-py3-none-any.whl # Download signatures from GitLab's generic package registry # Replace these values with your project's details: # GITLAB_URL: Your GitLab instance URL (for example, https://gitlab.com) # PROJECT_ID: Your project's ID number # PACKAGE_NAME: Your package name # VERSION: Package version (for example, 1.0.0) # FILENAME: The exact filename of your downloaded package curl --output "${FILENAME}.sig" \ "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${VERSION}/${FILENAME}.sig" curl --output "${FILENAME}.crt" \ "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${VERSION}/${FILENAME}.crt" -
서명 검증:
# Replace CERTIFICATE_IDENTITY and CERTIFICATE_OIDC_ISSUER with the values from the project's pipeline export CERTIFICATE_IDENTITY="https://gitlab.com/your-group/your-project//.gitlab-ci.yml@refs/heads/main" export CERTIFICATE_OIDC_ISSUER="https://gitlab.com" # Verify wheel package FILENAME="your-package-name-1.0.0-py3-none-any.whl" COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --signature "${FILENAME}.sig" \ --certificate "${FILENAME}.crt" \ --certificate-identity "${CERTIFICATE_IDENTITY}" \ --certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \ "${FILENAME}" # Verify source distribution FILENAME="your-package-name-1.0.0.tar.gz" COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --signature "${FILENAME}.sig" \ --certificate "${FILENAME}.crt" \ --certificate-identity "${CERTIFICATE_IDENTITY}" \ --certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \ "${FILENAME}"
최종 사용자로서 패키지를 검증할 때:
- 패키지 다운로드가 검증하려는 정확한 버전과 일치하는지 확인하세요.
- 각 패키지 유형(휠과 소스 배포)을 별도로 검증하세요.
- 인증서 ID가 패키지 서명에 사용된 것과 정확히 일치하는지 확인하세요.
- 모든 URL 구성 요소가 올바르게 설정되어 있는지 확인하세요. 예를 들어
GITLAB_URL또는PROJECT_ID. - 패키지 파일 이름이 레지스트리에 업로드된 것과 정확히 일치하는지 확인하세요.
- 키 없는 검증을 위해
COSIGN_EXPERIMENTAL=1기능 플래그를 사용하세요. 이 플래그는 필수입니다. - 검증 실패는 변조 또는 잘못된 인증서와 서명 쌍을 나타낼 수 있습니다.
- 프로젝트 파이프라인의 인증서 ID와 발급자 값을 추적하세요.
문제 해결#
이 튜토리얼을 완료할 때 다음 오류가 발생할 수 있습니다:
오류: 404 Not Found#
404 Not Found 오류 페이지가 발생하는 경우:
- 모든 URL 구성 요소를 다시 확인하세요.
- 레지스트리에 패키지 버전이 존재하는지 확인하세요.
- 버전 및 플랫폼 태그를 포함하여 파일 이름이 정확히 일치하는지 확인하세요.
검증 실패#
서명 검증이 실패하는 경우 다음을 확인하세요:
CERTIFICATE_IDENTITY가 서명 파이프라인과 일치하는지.CERTIFICATE_OIDC_ISSUER가 올바른지.- 서명과 인증서 쌍이 패키지에 맞는지.
권한 거부#
권한 문제가 발생하는 경우:
- 패키지 레지스트리에 액세스 권한이 있는지 확인하세요.
- 레지스트리가 비공개인 경우 인증을 확인하세요.
- Cosign 설치 시 올바른 파일 권한을 사용하세요.
인증 문제#
인증 문제가 발생하는 경우:
CI_JOB_TOKEN권한을 확인하세요.- 레지스트리 인증 구성을 확인하세요.
- 프로젝트의 액세스 설정을 검증하세요.
패키지 구성 및 파이프라인 설정 확인#
패키지 구성을 확인하세요. 다음을 확인하세요:
- 패키지 이름은 하이픈(
-)이 아닌 언더스코어(_)를 사용합니다. - 버전 문자열은 유효한 PEP 440을 사용합니다.
pyproject.toml파일이 올바르게 형식화되어 있습니다.
파이프라인 설정을 확인하세요. 다음을 확인하세요:
- OIDC가 올바르게 구성되어 있습니다.
- 작업 의존성이 올바르게 설정되어 있습니다.
- 필요한 권한이 갖춰져 있습니다.
