Runbook: CI/CD Secrets with SOPS

Example 1. Purpose

Configure SOPS-encrypted secrets for CI/CD pipelines (GitHub Actions, GitLab CI, Azure DevOps) using age keys or cloud KMS.

1. Prerequisites

Requirement Status Notes

SOPS installed locally

CHECK

sops --version

yq v4+ installed

CHECK

yq --version

age key pair

CHECK

ls ~/.secrets/.metadata/keys/master.age.*

Project repository

CHECK

Git repo where secrets will be stored

CI/CD platform access

CHECK

Admin access to configure secrets

2. Phase 1: Initialize Project

2.1. 1.1 Create SOPS Configuration

cd ~/projects/my-app

# Initialize with provider template
dsec cicd init . --github    # or --gitlab, --azure

Expected output:

✓ Initialized SOPS for CI/CD in .
  Add secrets: sops ./secrets/dev.yaml
  Provider template: github

2.2. 1.2 Verify Configuration

cat .sops.yaml

Expected:

# SOPS Configuration for CI/CD
# Generated by dsec cicd init

creation_rules:
  - path_regex: secrets/.*\.yaml$
    age: >-
      age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn

# GitHub Actions Usage:
# 1. Store AGE_SECRET_KEY in GitHub Secrets
# 2. In workflow:
#    - name: Decrypt secrets
#      run: |
#        echo "${{ secrets.AGE_SECRET_KEY }}" > /tmp/age.key
#        export SOPS_AGE_KEY_FILE=/tmp/age.key
#        sops -d secrets/prod.yaml > .env

2.3. 1.3 Validate Configuration

dsec cicd validate .

Expected:

→ Validating SOPS configuration...
✓ YAML syntax valid
✓ creation_rules present
✓ Encryption keys configured

✓ Configuration valid: ./.sops.yaml

3. Phase 2: Create Secrets

3.1. 2.1 Create Development Secrets

sops secrets/dev.yaml

Add your secrets in YAML format:

DATABASE_URL: postgres://user:pass@localhost:5432/myapp_dev
REDIS_URL: redis://localhost:6379
API_KEY: dev-api-key-12345
JWT_SECRET: dev-jwt-secret-67890

Save and exit. SOPS encrypts automatically.

3.2. 2.2 Create Production Secrets

sops secrets/prod.yaml
DATABASE_URL: postgres://prod-user:STRONG-PASS@db.example.com:5432/myapp
REDIS_URL: redis://redis.example.com:6379
API_KEY: prod-api-key-SECURE
JWT_SECRET: prod-jwt-VERY-SECRET

3.3. 2.3 Verify Encryption

# Should show encrypted content
cat secrets/prod.yaml

# Should show decrypted content
sops -d secrets/prod.yaml

4. Phase 3: Platform Configuration

4.1. 3.1 GitHub Actions

4.1.1. Store Age Key in GitHub Secrets

  1. Go to repo → Settings → Secrets and variables → Actions

  2. Click "New repository secret"

  3. Name: AGE_SECRET_KEY

  4. Value: Contents of ~/.secrets/.metadata/keys/master.age.key

# Copy your private key (DO NOT COMMIT THIS!)
cat ~/.secrets/.metadata/keys/master.age.key

4.1.2. Create Workflow

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install SOPS
        run: |
          SOPS_VERSION="3.8.1"
          curl -LO "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64"
          chmod +x "sops-v${SOPS_VERSION}.linux.amd64"
          sudo mv "sops-v${SOPS_VERSION}.linux.amd64" /usr/local/bin/sops

      - name: Decrypt secrets
        env:
          SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }}
        run: |
          # Create temporary key file
          echo "$SOPS_AGE_KEY" > /tmp/age.key
          export SOPS_AGE_KEY_FILE=/tmp/age.key

          # Decrypt to .env
          sops -d secrets/prod.yaml | yq -r 'to_entries | .[] | .key + "=" + .value' > .env

          # Cleanup key
          rm /tmp/age.key

      - name: Deploy
        run: |
          source .env
          echo "Deploying with DATABASE_URL=$DATABASE_URL"
          # ... your deploy commands ...
          rm .env

4.2. 3.2 GitLab CI

4.2.1. Store Age Key in CI/CD Variables

  1. Go to repo → Settings → CI/CD → Variables

  2. Add variable:

    • Key: AGE_SECRET_KEY

    • Value: Contents of master.age.key

    • Check: "Mask variable"

    • Check: "Protect variable" (optional)

4.2.2. Create Pipeline

# .gitlab-ci.yml
stages:
  - deploy

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    # Install SOPS
    - apk add --no-cache curl
    - curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
    - chmod +x sops-v3.8.1.linux.amd64
    - mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
    # Install yq
    - apk add --no-cache yq
  script:
    # Setup decryption
    - echo "$AGE_SECRET_KEY" > /tmp/age.key
    - export SOPS_AGE_KEY_FILE=/tmp/age.key

    # Decrypt secrets
    - sops -d secrets/prod.yaml > /tmp/secrets.yaml
    - export $(yq -r 'to_entries | .[] | .key + "=" + .value' /tmp/secrets.yaml | xargs)

    # Deploy
    - echo "DATABASE_URL is set: ${DATABASE_URL:0:20}..."

    # Cleanup
    - rm /tmp/age.key /tmp/secrets.yaml
  only:
    - main

4.3. 3.3 Azure DevOps

4.3.1. Store Age Key in Pipeline Variables

  1. Go to Pipelines → Library → Variable groups

  2. Create new group or add to existing

  3. Add variable: AGE_SECRET_KEY (mark as secret)

4.3.2. Create Pipeline

# azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: ubuntu-latest

steps:
  - script: |
      # Install SOPS
      curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
      chmod +x sops-v3.8.1.linux.amd64
      sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
    displayName: 'Install SOPS'

  - script: |
      echo "$(AGE_SECRET_KEY)" > /tmp/age.key
      export SOPS_AGE_KEY_FILE=/tmp/age.key
      sops -d secrets/prod.yaml > .env
      rm /tmp/age.key
    displayName: 'Decrypt secrets'
    env:
      AGE_SECRET_KEY: $(AGE_SECRET_KEY)

  - script: |
      source .env
      echo "Deploying..."
      # ... deploy commands ...
      rm .env
    displayName: 'Deploy'

5. Phase 4: Adding Cloud KMS (Optional)

For additional security, add cloud KMS alongside age keys.

5.1. 4.1 AWS KMS

# Add AWS KMS to project
dsec cicd add-kms . "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"

Update .sops.yaml:

creation_rules:
  - path_regex: secrets/.*\.yaml$
    kms: arn:aws:kms:us-east-1:123456789012:key/12345678-...
    age: >-
      age1wtdeuelfua4afrqqtw8claqf5wc335g7euhgh22pjzd57azpgq3q7jqcnn

Configure AWS credentials in CI:

# GitHub Actions
- name: Configure AWS
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/github-actions
    aws-region: us-east-1

5.2. 4.2 GCP KMS

dsec cicd add-kms . "projects/my-project/locations/global/keyRings/sops/cryptoKeys/sops-key"

5.3. 4.3 Azure Key Vault

dsec cicd add-kms . "https://my-vault.vault.azure.net/keys/sops-key/abc123"

6. Validation Checklist

Check Status How to Verify

.sops.yaml exists in repo

VERIFY

ls -la .sops.yaml

secrets/ directory exists

VERIFY

ls -la secrets/

Secrets are encrypted

VERIFY

cat secrets/prod.yaml shows encrypted data

Local decryption works

VERIFY

sops -d secrets/prod.yaml

AGE_SECRET_KEY in CI

VERIFY

Check platform secrets

CI pipeline runs

VERIFY

Trigger pipeline, check logs

Secrets not in logs

VERIFY

Review CI output for leaks

7. Security Best Practices

  1. Never commit age private keys - Only .sops.yaml and encrypted files

  2. Use masked variables - Always mask secret variables in CI/CD

  3. Cleanup temp files - Always rm key files after use

  4. Rotate keys periodically - Update age keys and re-encrypt

  5. Limit secret scope - Use environment-specific secrets (dev/staging/prod)

8. Troubleshooting

Issue Cause Solution

could not decrypt

Wrong key in CI

Verify AGE_SECRET_KEY content

no creation rules

Missing .sops.yaml

dsec cicd init .

permission denied

KMS IAM issue

Check cloud IAM permissions

Secrets in logs

Not masking output

Use ::add-mask:: or avoid echo