diff --git a/.gitignore b/.gitignore index 5ae47f3..9d4b336 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -# Secrets - NEVER commit these +# Secrets - NEVER commit plaintext, only encrypted .sops.yaml files secrets/**/*.yaml secrets/**/*.yml +!secrets/**/*.sops.yaml !secrets/.sops.yaml keys/age-key.txt *.key diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..f616714 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,7 @@ +# SOPS Configuration +# Defines encryption keys and rules for secret files + +creation_rules: + # All files in secrets/ directory + - path_regex: secrets/.*\.sops\.yaml$ + age: age170jqy5pg6z62kevadqyxxekw8ryf3e394zaquw0nhs9ae3v9wd6qq2hxnk diff --git a/keys/.gitignore b/keys/.gitignore new file mode 100644 index 0000000..6db72f5 --- /dev/null +++ b/keys/.gitignore @@ -0,0 +1,6 @@ +# NEVER commit Age private keys +*.txt +*.key + +# Only allow README +!README.md diff --git a/keys/README.md b/keys/README.md new file mode 100644 index 0000000..e044cae --- /dev/null +++ b/keys/README.md @@ -0,0 +1,52 @@ +# Age Encryption Keys + +⚠️ **CRITICAL**: This directory contains encryption keys that are **NOT committed to Git**. + +## Key Files + +- `age-key.txt` - Age private key for SOPS encryption (GITIGNORED) + +## Backup Checklist + +Before proceeding with any infrastructure work, ensure you have: + +- [ ] Copied `age-key.txt` to password manager +- [ ] Created offline backup (printed or encrypted USB) +- [ ] Verified backup can decrypt secrets successfully + +## Key Recovery + +If you lose access to `age-key.txt`: + +1. **Check password manager** for backup +2. **Check offline backups** (printed copy, USB drive) +3. **If no backup exists**: Secrets are PERMANENTLY LOST + - You will need to regenerate all secrets + - Re-encrypt all `.sops.yaml` files + - Update all services with new credentials + +## Generating a New Key + +Only do this if you've lost the original key or need to rotate for security: + +```bash +# Generate new Age key +age-keygen -o age-key.txt + +# Extract public key +grep "public key:" age-key.txt + +# Update .sops.yaml in repository root with new public key + +# Re-encrypt all secrets +cd .. +for file in secrets/**/*.sops.yaml; do + SOPS_AGE_KEY_FILE=keys/age-key.txt sops updatekeys -y "$file" +done +``` + +## Security Notes + +- This directory is in `.gitignore` +- Keys should never be shared via email, Slack, or unencrypted channels +- Always use secure methods for key distribution (password manager, encrypted channels) diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..3d0e7f5 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,234 @@ +# Secrets Management with SOPS + Age + +This directory contains encrypted secrets for the infrastructure using [SOPS](https://github.com/getsops/sops) with [Age](https://github.com/FiloSottile/age) encryption. + +## 🔐 Security Model + +- **Encryption**: All secret files encrypted with Age before committing to Git +- **Key Storage**: Age private key stored OUTSIDE this repository +- **Git-Safe**: Only encrypted files (.sops.yaml) are committed +- **Decryption**: Happens at runtime by Ansible or manually with `sops` + +## 📁 Directory Structure + +``` +secrets/ +├── README.md # This file +├── shared.sops.yaml # Shared secrets (encrypted) +└── clients/ + └── *.sops.yaml # Per-client secrets (encrypted) +``` + +## 🔑 Age Key Location + +**IMPORTANT**: The Age private key is stored at: +``` +keys/age-key.txt +``` + +This file is **gitignored** and must **NEVER** be committed. + +### Key Backup Checklist + +✅ **You MUST backup the Age key securely:** + +1. **Password Manager**: Store in Bitwarden/1Password/etc + ```bash + # Copy key content + cat keys/age-key.txt + # Store as secure note in password manager + ``` + +2. **Print Backup** (optional but recommended): + ```bash + # Print and store in secure physical location + cat keys/age-key.txt | lpr + ``` + +3. **Encrypted USB Drive** (optional): + ```bash + # Copy to encrypted USB for offline backup + cp keys/age-key.txt /Volumes/SecureUSB/infrastructure-age-key.txt + ``` + +⚠️ **WARNING**: If you lose this key, encrypted secrets are PERMANENTLY UNRECOVERABLE! + +## 🚀 Quick Start + +### Prerequisites + +```bash +# Install SOPS and Age +brew install sops age + +# Ensure you have the Age key +ls -la keys/age-key.txt +``` + +### View Encrypted Secrets + +```bash +# View shared secrets +SOPS_AGE_KEY_FILE=keys/age-key.txt sops secrets/shared.sops.yaml + +# View client secrets +SOPS_AGE_KEY_FILE=keys/age-key.txt sops secrets/clients/test.sops.yaml +``` + +### Edit Encrypted Secrets + +```bash +# Edit shared secrets (decrypts, opens $EDITOR, re-encrypts on save) +SOPS_AGE_KEY_FILE=keys/age-key.txt sops secrets/shared.sops.yaml + +# Edit client secrets +SOPS_AGE_KEY_FILE=keys/age-key.txt sops secrets/clients/test.sops.yaml +``` + +### Create New Client Secrets + +```bash +# Copy template +cp secrets/clients/test.sops.yaml secrets/clients/newclient.sops.yaml + +# Edit with generated passwords +SOPS_AGE_KEY_FILE=keys/age-key.txt sops secrets/clients/newclient.sops.yaml +``` + +### Generate Secure Passwords + +```bash +# Random 32-character password +openssl rand -base64 32 + +# Random 24-character password +openssl rand -base64 24 + +# Zitadel masterkey (32-byte hex) +openssl rand -hex 32 +``` + +## 🔧 Usage with Ansible + +Ansible automatically decrypts SOPS files using the `community.sops` collection. + +**In playbooks:** +```yaml +- name: Load client secrets + community.sops.load_vars: + file: "{{ playbook_dir }}/../secrets/clients/{{ client_name }}.sops.yaml" + name: client_secrets + +- name: Use decrypted secret + debug: + msg: "DB Password: {{ client_secrets.zitadel_db_password }}" +``` + +**Environment variable required:** +```bash +export SOPS_AGE_KEY_FILE=/path/to/infrastructure/keys/age-key.txt +``` + +## 📝 Secret File Structure + +### shared.sops.yaml + +Contains secrets shared across all infrastructure: +- Hetzner Cloud API token +- Hetzner Storage Box credentials +- ACME email for SSL certificates + +### clients/*.sops.yaml + +Per-client secrets: +- Database passwords (Zitadel, Nextcloud) +- Admin passwords +- Zitadel masterkey +- Restic repository password +- OIDC credentials (after generation) + +## 🛠️ Common Tasks + +### Decrypt to Temporary File + +```bash +# Decrypt for one-time use +SOPS_AGE_KEY_FILE=keys/age-key.txt sops --decrypt secrets/shared.sops.yaml > /tmp/secrets.yaml + +# Use the file +cat /tmp/secrets.yaml + +# IMPORTANT: Delete when done +rm /tmp/secrets.yaml +``` + +### Encrypt New File + +```bash +# Create plaintext file +cat > secrets/newfile.sops.yaml <