🔐 SOPS and Age Encryption
SOPS (Secrets OPerationS) combined with Age encryption provides a powerful, Git-friendly approach to managing encrypted configuration files and secrets.
🎯 SOPS and Age Overview
What is SOPS?
SOPS is an editor of encrypted files that supports YAML, JSON, ENV, INI and binary formats. It can encrypt specific keys/values rather than entire files.
What is Age?
Age is a simple, modern, and secure encryption tool with small explicit keys, no config options, and UNIX-style composability.
Why SOPS + Age?
- Git-friendly: Encrypted files can be committed to version control
- Format-preserving: Original file structure is maintained
- Selective encryption: Only sensitive fields are encrypted
- Easy key management: Age keys are simple strings
- No vendor lock-in: Works offline, no cloud dependencies
🔧 Setup and Configuration
Installing SOPS and Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | # Install SOPS
# macOS
brew install sops
# Ubuntu/Debian
sudo apt install sops
# Install Age
# macOS
brew install age
# Ubuntu/Debian
sudo apt install age
# Or download binaries
curl -L https://github.com/mozilla/sops/releases/download/v3.7.3/sops-v3.7.3.linux.amd64 -o sops
chmod +x sops
sudo mv sops /usr/local/bin/
curl -L https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz | tar xz
sudo mv age/age* /usr/local/bin/
|
Generating Age Keys
1
2
3
4
5
6
7
8
9
10
11
12
13 | # Generate a new age key pair
age-keygen -o age-key.txt
# Extract public key
age-keygen -y age-key.txt
# Set environment variable for SOPS
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
mkdir -p "$(dirname "$SOPS_AGE_KEY_FILE")"
cp age-key.txt "$SOPS_AGE_KEY_FILE"
# For multiple recipients
export SOPS_AGE_RECIPIENTS="age1publickey1,age1publickey2"
|
🔐 Encryption Workflows
Encrypting Configuration Files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | # Create a sample configuration file
cat > config.yaml << EOF
database:
host: prod-db.example.com
port: 5432
username: dbuser
password: ENC[AES256_GCM,data:...,tag:...,iv:...] # Will be encrypted
api:
endpoint: https://api.example.com
key: ENC[AES256_GCM,data:...,tag:...,iv:...] # Will be encrypted
logging:
level: info
file: /var/log/app.log
EOF
# Encrypt specific fields
sops --encrypt --age "$(age-keygen -y $SOPS_AGE_KEY_FILE)" config.yaml > config.enc.yaml
# Or let SOPS encrypt automatically marked fields
# Add 'ENC[]' markers to fields you want encrypted
|
Automatic Field Encryption
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | # Create a configuration with automatic encryption markers
cat > app-config.yaml << EOF
# SOPS will automatically encrypt fields ending with _key, _secret, or _password
app:
name: myapp
version: 1.0.0
database:
host: prod-db.example.com
port: 5432
username: dbuser
password: supersecretpassword # Will be encrypted automatically
api:
endpoint: https://api.example.com
api_key: abc123xyz # Will be encrypted automatically
secrets:
jwt_secret: jwtsecret123 # Will be encrypted automatically
encryption_key: enc123 # Will be encrypted automatically
public:
docs_url: https://docs.example.com
support_email: support@example.com
EOF
# Encrypt with SOPS rules
cat > .sops.yaml << EOF
creation_rules:
- path_regex: \.yaml$
age: >-
$(age-keygen -y $SOPS_AGE_KEY_FILE)
encrypted_suffix: _key
encrypted_regex: ^(password|secret|token)$
EOF
# Encrypt the file
sops --encrypt app-config.yaml > app-config.enc.yaml
|
Environment File Encryption
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | # Create environment file
cat > .env << EOF
# Public variables
APP_NAME=myapp
DEBUG=false
PORT=8080
# Sensitive variables (will be encrypted)
DATABASE_URL=postgresql://user:password@localhost:5432/db
API_KEY=sk-abc123xyz
JWT_SECRET=jwtsecret123
SMTP_PASSWORD=smtpsecret123
REDIS_URL=redis://:redispass@localhost:6379/0
EOF
# Encrypt environment file
sops --encrypt --age "$(age-keygen -y $SOPS_AGE_KEY_FILE)" .env > .env.enc
# Create SOPS config for automatic encryption
cat > .sops.yaml << EOF
creation_rules:
- path_regex: \.env$
age: >-
$(age-keygen -y $SOPS_AGE_KEY_FILE)
encrypted_suffix: _(URL|KEY|SECRET|PASSWORD|TOKEN)
EOF
sops --encrypt .env > .env.enc
|
🛡️ Secure Usage Patterns
Safe Decryption in Scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 | # Secure decryption function
decrypt_sops_file() {
local encrypted_file="$1"
local decrypted_file="$2"
if [ -z "$encrypted_file" ] || [ -z "$decrypted_file" ]; then
echo "Error: Encrypted and decrypted file paths required" >&2
return 1
fi
if [ ! -f "$encrypted_file" ]; then
echo "Error: Encrypted file not found: $encrypted_file" >&2
return 1
fi
# Create secure temporary file
local temp_file
temp_file=$(mktemp "${TMPDIR:-/tmp}/sops_decrypt_XXXXXX") || {
echo "Error: Failed to create temporary file" >&2
return 1
}
chmod 600 "$temp_file"
# Decrypt to temporary file
if SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --decrypt "$encrypted_file" > "$temp_file"; then
# Move to final location
mv "$temp_file" "$decrypted_file"
chmod 600 "$decrypted_file"
echo "File decrypted to $decrypted_file"
return 0
else
rm -f "$temp_file"
echo "Error: Failed to decrypt $encrypted_file" >&2
return 1
fi
}
# Usage in deployment scripts
DECRYPTED_CONFIG=$(mktemp)
trap 'rm -f "$DECRYPTED_CONFIG"' EXIT
if decrypt_sops_file "config.enc.yaml" "$DECRYPTED_CONFIG"; then
# Use decrypted config
myapp --config "$DECRYPTED_CONFIG"
else
echo "Failed to decrypt configuration" >&2
exit 1
fi
|
Environment Variable Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | # Load encrypted environment variables
load_sops_env() {
local encrypted_env_file="$1"
if [ -z "$encrypted_env_file" ]; then
echo "Error: Encrypted environment file required" >&2
return 1
fi
# Decrypt and source environment variables
local decrypted_content
decrypted_content=$(SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --decrypt "$encrypted_env_file" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "Error: Failed to decrypt environment file $encrypted_env_file" >&2
return 1
fi
# Export variables (be careful with sensitive ones)
echo "$decrypted_content" | while IFS= read -r line; do
# Skip comments and empty lines
if [[ ! "$line" =~ ^[[:space:]]*# ]] && [[ -n "$line" ]]; then
# Export the variable
export "$line"
fi
done
echo "Environment variables loaded from $encrypted_env_file"
}
# Usage
load_sops_env ".env.enc"
|
Git Integration Hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | # Pre-commit hook to prevent committing unencrypted secrets
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
# Check for unencrypted sensitive files
if git diff --cached --name-only | grep -E '\.(env|yaml|json)$'; then
echo "Checking for unencrypted secrets..."
# Check if any staged files contain sensitive patterns
for file in $(git diff --cached --name-only | grep -E '\.(env|yaml|json)$'); do
if [ -f "$file" ] && grep -qE '(password|secret|key|token).*[[:alnum:]]{8,}' "$file"; then
# Check if it's SOPS encrypted
if ! grep -q 'sops:' "$file" 2>/dev/null; then
echo "Error: Unencrypted sensitive data found in $file"
echo "Please encrypt with SOPS before committing"
exit 1
fi
fi
done
fi
echo "Pre-commit check passed"
EOF
chmod +x .git/hooks/pre-commit
|
🔄 Key Management and Rotation
Managing Multiple Recipients
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | # Create SOPS config for multiple recipients
cat > .sops.yaml << EOF
creation_rules:
- path_regex: \.(yaml|json|env)$
age: >-
age1recipient1publickey,
age1recipient2publickey,
age1recipient3publickey
encrypted_regex: ^(password|secret|key|token)$
EOF
# Rotate keys for existing files
rotate_sops_keys() {
local file_pattern="${1:-*.enc.*}"
find . -name "$file_pattern" -type f | while read -r file; do
echo "Rotating keys for $file"
# Decrypt and re-encrypt with new keys
local temp_decrypted
temp_decrypted=$(mktemp)
if SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --decrypt "$file" > "$temp_decrypted"; then
# Re-encrypt with current configuration
SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --encrypt "$temp_decrypted" > "${file}.new"
mv "${file}.new" "$file"
echo "Keys rotated for $file"
else
echo "Failed to rotate keys for $file" >&2
fi
rm -f "$temp_decrypted"
done
}
# Usage
rotate_sops_keys "*.enc.yaml"
|
Key Revocation and Removal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | # Remove a recipient from encrypted files
remove_sops_recipient() {
local old_recipient="$1"
local file_pattern="${2:-*.enc.*}"
if [ -z "$old_recipient" ]; then
echo "Error: Recipient public key required" >&2
return 1
fi
# Update SOPS configuration to remove recipient
# This requires manual editing of .sops.yaml
# Re-encrypt all files without the old recipient
find . -name "$file_pattern" -type f | while read -r file; do
echo "Updating $file to remove recipient"
local temp_decrypted
temp_decrypted=$(mktemp)
# Decrypt with old keys
SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --decrypt "$file" > "$temp_decrypted"
# Re-encrypt with new configuration (without old recipient)
SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --encrypt "$temp_decrypted" > "${file}.new"
mv "${file}.new" "$file"
rm -f "$temp_decrypted"
done
echo "Recipient $old_recipient removed from all files"
}
|
🧪 Testing and Validation
SOPS File Validation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | # Validate SOPS encrypted files
validate_sops_file() {
local file="$1"
if [ -z "$file" ]; then
echo "Error: File path required" >&2
return 1
fi
if [ ! -f "$file" ]; then
echo "Error: File not found: $file" >&2
return 1
fi
# Check if file is SOPS encrypted
if ! grep -q 'sops:' "$file" 2>/dev/null; then
echo "Warning: File $file is not SOPS encrypted" >&2
return 1
fi
# Try to decrypt (dry run)
if SOPS_AGE_KEY_FILE="$SOPS_AGE_KEY_FILE" sops --decrypt "$file" >/dev/null 2>&1; then
echo "File $file is valid and can be decrypted"
return 0
else
echo "Error: File $file cannot be decrypted" >&2
return 1
fi
}
# Validate all encrypted files
validate_all_sops_files() {
find . -name "*.enc.*" -type f | while read -r file; do
validate_sops_file "$file"
done
}
|
Mock SOPS for Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 | # Create mock SOPS for local testing
mock_sops() {
local mock_dir="${1:-/tmp/mock_sops}"
mkdir -p "$mock_dir"
# Create mock SOPS script
cat > "$mock_dir/sops" << 'EOF'
#!/bin/bash
# Simple mock SOPS for testing
ACTION=""
INPUT_FILE=""
OUTPUT_FILE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--encrypt)
ACTION="encrypt"
shift
;;
--decrypt)
ACTION="decrypt"
shift
;;
--age)
# Skip age key
shift 2
;;
-*)
# Skip other flags
shift 2
;;
*)
INPUT_FILE="$1"
shift
;;
esac
done
case "$ACTION" in
encrypt)
if [[ -n "$INPUT_FILE" && -f "$INPUT_FILE" ]]; then
# Simple encryption simulation
echo "# This is a mock encrypted file" > "${INPUT_FILE}.mock"
echo "sops:" >> "${INPUT_FILE}.mock"
echo " mock_encrypted: true" >> "${INPUT_FILE}.mock"
cat "$INPUT_FILE" | sed 's/^/# /' >> "${INPUT_FILE}.mock"
echo "Mock encrypted: $INPUT_FILE -> ${INPUT_FILE}.mock"
else
echo "Mock SOPS: No input file for encryption" >&2
exit 1
fi
;;
decrypt)
if [[ -n "$INPUT_FILE" && -f "$INPUT_FILE" ]]; then
# Simple decryption simulation
grep -v '^#' "$INPUT_FILE" | grep -v '^sops:' | sed 's/^# //'
else
echo "Mock SOPS: No input file for decryption" >&2
exit 1
fi
;;
*)
echo "Mock SOPS: Unknown action $ACTION" >&2
exit 1
;;
esac
EOF
chmod +x "$mock_dir/sops"
echo "Mock SOPS created in $mock_dir"
echo "Use: export PATH=\"$mock_dir:\$PATH\" for testing"
}
|
🧾 Summary
✅ Git-friendly encryption that preserves file structure
✅ Selective field encryption for fine-grained control
✅ Simple key management with Age encryption
✅ Secure usage patterns for scripts and CI/CD
✅ Key rotation and revocation capabilities
✅ Comprehensive validation and testing support
🧾 See Also