Runbook: CI/CD Secrets with SOPS
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 |
|
yq v4+ installed |
CHECK |
|
age key pair |
CHECK |
|
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
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.
4. Phase 3: Platform Configuration
4.1. 3.1 GitHub Actions
4.1.1. Store Age Key in GitHub Secrets
-
Go to repo → Settings → Secrets and variables → Actions
-
Click "New repository secret"
-
Name:
AGE_SECRET_KEY -
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
-
Go to repo → Settings → CI/CD → Variables
-
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
-
Go to Pipelines → Library → Variable groups
-
Create new group or add to existing
-
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
6. Validation Checklist
| Check | Status | How to Verify |
|---|---|---|
|
VERIFY |
|
|
VERIFY |
|
Secrets are encrypted |
VERIFY |
|
Local decryption works |
VERIFY |
|
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
|