Secrets in CI/CD Pipelines
Secrets in CI/CD Pipelines
Build-time secrets require special handling to avoid exposure in images or logs. Multi-stage builds should use secrets only in build stages, not runtime stages. Build arguments containing secrets must not be used in cacheable layers. BuildKit's secret mounting provides secure secret access during builds. CI/CD systems need secure secret injection without exposing values in build logs.
Pipeline secret management requires integration between CI/CD platforms and secret stores. Jenkins credentials, GitLab CI/CD variables, and GitHub Actions secrets provide basic secret storage. However, production deployments benefit from external secret management integration. Dynamic secret retrieval during pipeline execution reduces secret exposure in CI/CD systems.
# Example: Secure CI/CD pipeline with external secrets
# .gitlab-ci.yml with HashiCorp Vault integration
stages:
- build
- test
- scan
- deploy
variables:
VAULT_ADDR: https://vault.company.com:8200
DOCKER_BUILDKIT: 1
.vault_auth:
before_script:
- export VAULT_TOKEN=$(vault write -field=token auth/jwt/login role=gitlab-ci jwt=$CI_JOB_JWT)
- export DATABASE_PASSWORD=$(vault kv get -field=password secret/ci/database)
- export REGISTRY_PASSWORD=$(vault kv get -field=password secret/ci/registry)
build:
extends: .vault_auth
stage: build
script:
# Login to registry using Vault credentials
- echo "$REGISTRY_PASSWORD" | docker login -u $REGISTRY_USER --password-stdin $CI_REGISTRY
# Build with secrets mounted (not embedded)
- |
DOCKER_BUILDKIT=1 docker build \
--secret id=db_pass,env=DATABASE_PASSWORD \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg VCS_REF=$CI_COMMIT_SHA \
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
-f Dockerfile .
# Push to registry
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Cleanup
- docker logout $CI_REGISTRY
- unset DATABASE_PASSWORD REGISTRY_PASSWORD
security_scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
deploy:
extends: .vault_auth
stage: deploy
environment:
name: production
url: https://app.company.com
script:
# Get production secrets from Vault
- export PROD_DB_PASSWORD=$(vault kv get -field=password secret/prod/database)
- export PROD_API_KEY=$(vault kv get -field=api_key secret/prod/external-api)
# Create Kubernetes secrets
- |
kubectl create secret generic webapp-secrets \
--from-literal=db-password=$PROD_DB_PASSWORD \
--from-literal=api-key=$PROD_API_KEY \
--dry-run=client -o yaml | kubectl apply -f -
# Deploy application
- kubectl set image deployment/webapp webapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Cleanup
- unset PROD_DB_PASSWORD PROD_API_KEY
only:
- main