Managing secrets like API keys, database passwords, and encryption keys is one of the most critical challenges in software development. This guide walks you through setting up a secure, auditable, and team-friendly secrets management system using SOPS and AWS KMS.
The Problem with Traditional Secrets Management
Most teams start with one of these approaches:
- Environment variables - Easy to leak, hard to track changes
- Shared password managers - No version control, manual sync
- Plain text in private repos - One breach exposes everything
- AWS Secrets Manager only - No code review for changes, no Git history
Each has significant drawbacks for growing teams.
A Better Approach: SOPS + AWS KMS + Git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────────────┐
│ The Solution │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────┐
│ AWS KMS │
│ (Master Encryption Key) │
└─────────┬─────────────────┘
│
encrypt/decrypt requests with SOPS
│
▼
┌─────────┐ push ┌──────────────┐ terraform ┌─────────────────┐
│ Editor │ ◄──────────► │ .enc.json │ ────apply───► │ Secrets Manager │
│ (plain) │ │ (encrypted) │ │ (for apps) │
└─────────┘ └──────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
In memory only Stored in Git Apps read from here
(never on disk) (safe to commit)
Why This Works
| Benefit | Description |
|---|---|
| Version controlled | Full Git history of all secret changes |
| Code review | PRs show which keys changed (not values) |
| Access control | IAM controls who can decrypt |
| Audit trail | CloudTrail logs every KMS access |
| Easy revocation | Remove IAM access = instant revoke |
| No plain text | Secrets never written to disk unencrypted |
Step 1: Create a KMS Key
First, create a dedicated KMS key for secrets encryption:
1
aws kms create-key --description "Secrets encryption key for SOPS"
Create an alias for easier reference:
1
2
3
aws kms create-alias \
--alias-name alias/sops-secrets \
--target-key-id <key-id-from-above>
Step 2: Set Up IAM Permissions
Create an IAM policy for team members who need access:
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:REGION:ACCOUNT_ID:key/KEY_ID"
}
]
}
Attach this policy to:
- Individual IAM users who need to edit secrets
- CI/CD roles that deploy secrets
Step 3: Install SOPS
On macOS:
1
brew install sops
On Linux:
1
2
3
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
Step 4: Create Repository Structure
1
2
3
mkdir secrets-management
cd secrets-management
git init
Create the SOPS configuration file .sops.yaml:
1
2
3
creation_rules:
- path_regex: secrets/.*\.enc\.json$
kms: arn:aws:kms:REGION:ACCOUNT_ID:key/KEY_ID
Create .gitignore:
1
2
3
4
5
6
7
8
# Decrypted files (should never exist, but just in case)
*.dec.json
*.dec.yaml
# Terraform
.terraform/
*.tfstate
*.tfstate.backup
Create the secrets directory:
1
mkdir -p secrets
Step 5: Create Your First Encrypted Secret
1
sops secrets/development.enc.json
This opens your editor. Add your secrets:
1
2
3
4
5
{
"DATABASE_URL": "postgres://user:pass@localhost:5432/db",
"API_KEY": "your-secret-api-key",
"JWT_SECRET": "your-jwt-secret"
}
Save and close. SOPS encrypts automatically.
Using VS Code
1
EDITOR="code --wait" sops secrets/development.enc.json
Step 6: Set Up Terraform for Deployment
Create main.tf:
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
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "secrets/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
sops = {
source = "carlpett/sops"
version = "~> 1.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "sops" {}
# Read encrypted file
data "sops_file" "api_development" {
source_file = "secrets/development.enc.json"
}
# Create secret in AWS Secrets Manager
resource "aws_secretsmanager_secret" "api_development" {
name = "api-secrets/development"
tags = {
ManagedBy = "terraform"
}
}
resource "aws_secretsmanager_secret_version" "api_development" {
secret_id = aws_secretsmanager_secret.api_development.id
secret_string = data.sops_file.api_development.raw
}
Step 7: Deploy
1
2
3
terraform init
terraform plan
terraform apply
The Developer Workflow
Updating a Secret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. Create a branch
git checkout -b update-api-key
# 2. Edit the secret
EDITOR="code --wait" sops secrets/development.enc.json
# 3. Commit and push
git add secrets/
git commit -m "Rotate API key for payment provider"
git push -u origin update-api-key
# 4. Create PR for review
# 5. After merge, apply
git checkout main
git pull
terraform apply
What Reviewers See
In the PR diff, reviewers see:
1
2
3
4
5
6
7
"sops": {
- "lastmodified": "2024-01-xxT10:00:00Z",
+ "lastmodified": "2024-01-xxT14:30:00Z",
...
},
- "API_KEY": "ENC[Axxxxxxxxx:...,type:str]",
+ "API_KEY": "ENC[Axxxxxxxxx:...,type:str]",
They can see:
- Which keys changed
- When it changed
- Who changed it
They cannot see:
- Actual secret values
Security Best Practices
1. Separate Keys by Environment
Use different KMS keys for different environments:
1
2
3
4
5
6
# .sops.yaml
creation_rules:
- path_regex: secrets/development/.*\.enc\.json$
kms: arn:aws:kms:us-east-1:ACCOUNT:key/DEV_KEY_ID
- path_regex: secrets/production/.*\.enc\.json$
kms: arn:aws:kms:us-east-1:ACCOUNT:key/PROD_KEY_ID
2. Limit Production Access
Only give production KMS access to:
- Senior engineers
- CI/CD deployment roles
3. Enable KMS Key Rotation
1
aws kms enable-key-rotation --key-id KEY_ID
4. Monitor with CloudTrail
All KMS operations are logged. Set up alerts for:
- Failed decryption attempts
- Unusual access patterns
- Access from new IP addresses
Revoking Access
When someone leaves the team:
1
2
3
4
# Remove their IAM policy attachment
aws iam detach-user-policy \
--user-name departed-user \
--policy-arn arn:aws:iam::ACCOUNT:policy/sops-kms-access
Immediately, they can no longer decrypt secrets. No need to:
- Rotate the KMS key
- Re-encrypt all secrets
- Change any passwords
Comparison: Before and After
| Aspect | Before (Plain Text) | After (SOPS + KMS) |
|---|---|---|
| Git storage | Unsafe | Safe (encrypted) |
| Code review | Shows actual secrets | Shows only key names |
| Access control | All or nothing | Fine-grained IAM |
| Audit | None | Full CloudTrail logs |
| Offboarding | Rotate everything | Remove IAM access |
| Compliance | Fails audits | Passes audits |
Cost
| Component | Monthly Cost |
|---|---|
| KMS key | ~$1/month per key |
| KMS requests | ~$0.03 per 10,000 requests |
| Secrets Manager | ~$0.40 per secret per month |
For most teams: Under $10/month total.
Conclusion
SOPS + AWS KMS gives you:
- Git-native workflow - Version control, PRs, and code review
- Strong encryption - AWS KMS with audit trails
- Simple access control - IAM policies
- Easy revocation - Remove IAM access instantly
- Compliance ready - Audit logs and access controls
The initial setup takes about an hour. The long-term benefits—security, auditability, and team workflow—are well worth it.
Quick Reference
| Task | Command |
|---|---|
| Create/edit secret | EDITOR="code --wait" sops secrets/file.enc.json |
| View secret | sops -d secrets/file.enc.json |
| Deploy to AWS | terraform apply |
| Rotate KMS key | aws kms enable-key-rotation --key-id KEY_ID |
This guide covers the essential setup. For production deployments, consider adding CI/CD automation, multi-region replication, and automated secret rotation.