From 6bc8e508c66de84d7b203a4d77df114e0e54ca9b Mon Sep 17 00:00:00 2001 From: Pieter Date: Sat, 27 Dec 2025 14:23:36 +0100 Subject: [PATCH] Complete SOPS secrets management setup (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completed Issue #5: SOPS Secrets Management All objectives met: - ✅ Age encryption key generated (keys/age-key.txt) - ✅ SOPS configured with Age backend (.sops.yaml) - ✅ Secrets directory structure created - ✅ Example encrypted secrets (shared + test client) - ✅ Comprehensive documentation for key backup - ✅ Ready for Ansible integration Security measures: - Age private key gitignored (keys/age-key.txt) - Only encrypted .sops.yaml files committed - Plaintext secrets explicitly excluded - Key backup procedures documented Files added: - .sops.yaml - SOPS configuration with Age public key - secrets/shared.sops.yaml - Shared secrets (encrypted) - secrets/clients/test.sops.yaml - Test client secrets (encrypted) - secrets/README.md - Complete SOPS usage guide - keys/README.md - Key backup procedures - keys/.gitignore - Protects private keys Updated: - .gitignore - Allow .sops.yaml, block plaintext Tested: - Encryption: ✅ Files encrypted successfully - Decryption: ✅ Secrets decrypt correctly - Git safety: ✅ Private key excluded from commits Next: Ready for Zitadel/Nextcloud deployment with secure credentials Closes #5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 3 +- .sops.yaml | 7 + keys/.gitignore | 6 + keys/README.md | 52 ++++++++ secrets/README.md | 234 +++++++++++++++++++++++++++++++++ secrets/clients/test.sops.yaml | 32 +++++ secrets/shared.sops.yaml | 25 ++++ 7 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 .sops.yaml create mode 100644 keys/.gitignore create mode 100644 keys/README.md create mode 100644 secrets/README.md create mode 100644 secrets/clients/test.sops.yaml create mode 100644 secrets/shared.sops.yaml 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 <