Post-Tyranny-Tech-Infrastru.../secrets
Pieter 282e248605 Security: Remove exposed Collabora password from docs, rotate credential
Security fixes:
- Remove hardcoded Collabora password from COLLABORA_SETUP.md
- Replace with placeholder and password generation instructions
- Rotate exposed Collabora password in test.sops.yaml
- New password: NX3NEpOMogUOcADjB0B2y1QGuRTSeDUn (SOPS encrypted)

The old password was exposed in documentation and needs to be
rotated on the test server. Future deployments will use the new
password from the encrypted secrets file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-06 10:54:14 +01:00
..
clients Security: Remove exposed Collabora password from docs, rotate credential 2026-01-06 10:54:14 +01:00
README.md Complete SOPS secrets management setup (#5) 2025-12-27 14:23:36 +01:00
shared.sops.yaml Complete SOPS secrets management setup (#5) 2025-12-27 14:23:36 +01:00

Secrets Management with SOPS + Age

This directory contains encrypted secrets for the infrastructure using SOPS with 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

    # Copy key content
    cat keys/age-key.txt
    # Store as secure note in password manager
    
  2. Print Backup (optional but recommended):

    # Print and store in secure physical location
    cat keys/age-key.txt | lpr
    
  3. Encrypted USB Drive (optional):

    # 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

# Install SOPS and Age
brew install sops age

# Ensure you have the Age key
ls -la keys/age-key.txt

View Encrypted Secrets

# 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

# 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

# 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

# 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:

- 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:

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

# 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

# Create plaintext file
cat > secrets/newfile.sops.yaml <<EOF
my_secret: "super-secret-value"
EOF

# Encrypt in place
SOPS_AGE_KEY_FILE=keys/age-key.txt sops --encrypt --in-place secrets/newfile.sops.yaml

Re-encrypt with New Key

If you need to rotate the Age key:

# Generate new key
age-keygen -o keys/age-key-new.txt

# Get public key
grep "public key:" keys/age-key-new.txt

# Update .sops.yaml with new public key

# Re-encrypt all files
for file in secrets/**/*.sops.yaml; do
  SOPS_AGE_KEY_FILE=keys/age-key.txt sops updatekeys -y "$file"
done

# Replace old key
mv keys/age-key.txt keys/age-key-old.txt
mv keys/age-key-new.txt keys/age-key.txt

🔍 Troubleshooting

"Failed to get the data key required to decrypt the SOPS file"

  • Cause: Age private key not found or incorrect
  • Fix: Ensure SOPS_AGE_KEY_FILE points to correct key
    export SOPS_AGE_KEY_FILE=/full/path/to/keys/age-key.txt
    

"no matching creation rules found"

  • Cause: File path doesn't match .sops.yaml regex
  • Fix: Ensure filename ends with .sops.yaml

"config file not found"

  • Cause: .sops.yaml not in repository root
  • Fix: Check .sops.yaml exists at repo root

🔒 Security Best Practices

  1. Never commit keys/age-key.txt
  2. Always encrypt before committing secrets
  3. Backup the key in multiple secure locations
  4. Use strong passwords: minimum 24 characters
  5. Rotate secrets periodically
  6. Limit key access to essential personnel only
  7. Delete temp files after decryption

📚 References