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