← Back to Guide Index

Complete Secrets Setup Guide

This guide walks through of complete workflow for setting up and managing encrypted secrets with sops-nix. It solves "chicken and egg" problem of needing secrets before your server is fully configured.

Table of Contents

1. The Bootstrap Problem

2. Strategy 1: Generate Secrets After Install (Recommended)

3. Strategy 2: Pre-generate Secrets Locally

4. Setting Up Your Encryption Key

5. Creating and Managing Secrets

6. Service-Specific Secret Examples

7. Advanced: Multiple Recipients

8. Rotating Secrets

9. Backup and Recovery

The Bootstrap Problem

The dilemma:

Two strategies to solve this:

Strategy 1: Generate Secrets After Install (Recommended)

This is simplest approach for most users.

Step 1: Install NixOS Without Secrets

First, install NixOS with a minimal configuration that doesn't need secrets:

# /etc/nixos/configuration.nix (initial version)
{
  # ... basic system config without services that need secrets ...
  
  # Don't configure PDS, email, or other secret-needing services yet
}
}}}

=== Step 2: Get Your Server's SSH Key ===

After NixOS is running:

# On server, get your SSH host public key

cat /etc/ssh/sshhosted25519_key.pub

# Copy this - it looks like:

# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... root@hostname

}}}

Step 3: Convert to Age Key

On your *local machine*:

# Install ssh-to-age if not already installed
# macOS: brew install ssh-to-age
# Linux: download from https://github.com/Mic92/ssh-to-age/releases

# Convert SSH key to age format
ssh-to-age -i <(echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...")

# Output example:
# age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
}}}

=== Step 4: Create .sops.yaml ===

On your **local machine**, create sops configuration:

mkdir -p /etc/nixos/secrets

cat > /etc/nixos/secrets/.sops.yaml << 'EOF'

keys:

creation_rules:

key_groups:

EOF

}}}

Replace age key with your actual key from Step 3.

Step 5: Generate Secrets

Now generate actual secrets. Here's how for each type:

Generate PDS JWT Secret

# Generate a random 32-byte hex string
openssl rand -hex 32

# Or use pwgen
pwgen -s 64 1

# Save this - you'll encrypt it with sops
}}}

==== Generate PDS Admin Password ===

# Generate strong password

pwgen -s 32 1

# or

openssl rand -base64 24

}}}

Generate PDS PLC Rotation Key

# Generate K256 private key
openssl ecparam -name secp256k1 -genkey -noout -out /tmp/plc.key

# Convert to hex format needed by PDS
openssl ec -in /tmp/plc.key -outform DER | tail -c 32 | xxd -p -c 64

# Clean up
rm /tmp/plc.key
}}}

==== Create Email SMTP URL ===

Format: `smtps://username:password@smtp.gmail.com:465`

For Gmail:
- Create an app-specific password at https://myaccount.google.com/apppasswords
- URL encode special characters in password (replace `@` with `%40`, etc.)

=== Step 6: Create Encrypted Secrets File ===

Create secrets file with all values:

cat > /etc/nixos/secrets/bluesky-pds.yaml << 'EOF'

PDSJWTSECRET: "your-generated-jwt-secret-here"

PDSADMINPASSWORD: "your-admin-password-here"

PDSPLCROTATIONKEYK256PRIVATEKEY_HEX: "your-plc-key-hex-here"

PDSEMAILSMTP_URL: "smtps://user:pass@smtp.gmail.com:465"

PDSEMAILFROM_ADDRESS: "noreply@yourdomain.com"

EOF

# Encrypt it

sops encrypt -i /etc/nixos/secrets/bluesky-pds.yaml

}}}

Step 7: Update Configuration

Add sops configuration to your NixOS config:

{
  # ... other config ...

  sops.defaultSopsFile = ./secrets/bluesky-pds.yaml;
  sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];

  # Define secrets (empty attrs = default permissions)
  sops.secrets.PDS_JWT_SECRET = {};
  sops.secrets.PDS_ADMIN_PASSWORD = {};
  sops.secrets.PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = {};
  sops.secrets.PDS_EMAIL_SMTP_URL = {};
  sops.secrets.PDS_EMAIL_FROM_ADDRESS = {};

  # Create environment file from secrets
  sops.templates."pds-env".content = ''
    PDS_JWT_SECRET=${config.sops.placeholder.PDS_JWT_SECRET}
    PDS_ADMIN_PASSWORD=${config.sops.placeholder.PDS_ADMIN_PASSWORD}
    PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${config.sops.placeholder.PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}
    PDS_EMAIL_SMTP_URL=${config.sops.placeholder.PDS_EMAIL_SMTP_URL}
    PDS_EMAIL_FROM_ADDRESS=${config.sops.placeholder.PDS_EMAIL_FROM_ADDRESS}
  '';

  # Now you can configure PDS
  services.bluesky-pds = {
    enable = true;
    environmentFiles = [ config.sops.templates."pds-env".path ];
    # ... other settings ...
  };
}
}}}

=== Step 8: Deploy ===

cd /etc/nixos

sudo nixos-rebuild switch --flake .#snek

}}}

Verify secrets are decrypted:

sudo ls -la /run/secrets/
sudo cat /run/secrets/PDS_JWT_SECRET
}}}

== Strategy 2: Pre-generate Secrets Locally ==

Use this if you want everything ready before touching server.

=== Step 1: Generate Age Key Locally ===

# Generate a new age key pair

age-keygen -o ~/snek-server-key.txt

# This creates a file with:

# # created: 2024-01-15T10:00:00Z

# # public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

# AGE-SECRET-KEY-1...

}}}

Save public key and secret key file securely.

Step 2: Create Secrets with Local Key

Create .sops.yaml using your local age key:

keys:
  - &local age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

creation_rules:
  - path_regex: secrets/.*\.yaml$
    key_groups:
      - age:
          - *local
}}}

Create and encrypt secrets on your local machine.

=== Step 3: Install NixOS with Age Key ===

During NixOS installation, copy your age secret key to server:

# On server during install

mkdir -p /mnt/etc/secrets

cp ~/snek-server-key.txt /mnt/etc/secrets/age.key

chmod 600 /mnt/etc/secrets/age.key

}}}

Step 4: Configure sops-nix to Use File

{
  sops.age.keyFile = "/etc/secrets/age.key";
  # ... rest of sops config ...
}
}}}

=== Step 5: After Install, Add Server's SSH Key ===

Once installed, get server's SSH key and add it as a recipient:

# On server

cat /etc/ssh/sshhosted25519_key.pub | ssh-to-age

# Add this key to .sops.yaml as additional recipient

# Then rotate (see Rotating Secrets section)

}}}

Setting Up Your Encryption Key

Understanding Age Keys

Age uses X25519 keys:

Converting SSH Keys

Your NixOS server already has SSH host keys at /etc/ssh/sshhosted25519_key.

# Convert SSH public key to age public key
ssh-to-age -i /etc/ssh/ssh_host_ed25519_key.pub

# Convert SSH private key to age secret key (not usually needed)
ssh-to-age -private-key -i /etc/ssh/ssh_host_ed25519_key
}}}

=== Using Multiple Keys ===

You can have multiple recipients who can decrypt:

keys:

creation_rules:

key_groups:

}}}

Creating and Managing Secrets

Creating a New Secret File

# Method 1: Create then encrypt
cat > secrets/new-service.yaml << 'EOF'
API_KEY: "secret-value-here"
DATABASE_URL: "postgres://user:pass@localhost/db"
EOF

sops encrypt -i secrets/new-service.yaml

# Method 2: Use sops directly (opens in $EDITOR)
sops secrets/new-service.yaml
}}}

=== Editing Existing Secrets ===

# Decrypt, open in editor, re-encrypt automatically

sops secrets/bluesky-pds.yaml

# Make changes, save, and exit

# sops handles encryption automatically

}}}

Viewing Decrypted Secrets

# Decrypt and output to stdout
sops -d secrets/bluesky-pds.yaml

# Extract specific value
sops -d --extract '["PDS_JWT_SECRET"]' secrets/bluesky-pds.yaml
}}}

=== Adding New Secrets to Existing File ===

# Edit file

sops secrets/bluesky-pds.yaml

# Add new key-value pair in editor:

# NEW_SECRET: "new-value"

# Save and exit - automatically encrypted

}}}

Changing Secret Values

# Edit file
sops secrets/bluesky-pds.yaml

# Change values in editor
# Save and exit

# Rebuild NixOS to pick up new values
sudo nixos-rebuild switch --flake .#snek
}}}

== Service-Specific Secret Examples ==

=== Bluesky PDS ===

# secrets/bluesky-pds.yaml

PDSJWTSECRET: "64-char-hex-string"

PDSADMINPASSWORD: "strong-admin-password"

PDSPLCROTATIONKEYK256PRIVATEKEY_HEX: "64-char-hex-private-key"

PDSEMAILSMTP_URL: "smtps://user:pass@smtp.gmail.com:465"

PDSEMAILFROM_ADDRESS: "noreply@yourdomain.com"

}}}

Generate JWT Secret:

openssl rand -hex 32
}}}

*Generate Admin Password:*

pwgen -s 32 1

}}}

Generate PLC Key:

openssl ecparam -name secp256k1 -genkey -noout | openssl ec -outform DER | tail -c 32 | xxd -p -c 64
}}}

=== PostgreSQL ===

# secrets/postgresql.yaml

POSTGRES_PASSWORD: "db-password"

}}}

Custom Service API Keys

# secrets/external-services.yaml
GITHUB_TOKEN: "ghp_xxxxxxxx"
STRIPE_SECRET_KEY: "sk_live_xxxxxxxx"
SENDGRID_API_KEY: "SG.xxxxxxxx"
}}}

=== WireGuard/Tailscale Keys ===

# secrets/networking.yaml

TAILSCALEAUTHKEY: "tskey-auth-xxxxxxxx"

WIREGUARDPRIVATEKEY: "xxxxxxxx"

}}}

Advanced: Multiple Recipients

Why Multiple Recipients?

Adding a New Recipient

1. Get their age public key

2. Add to .sops.yaml:

keys:
  - &server age1server...
  - &backup age1backup...
  - &newadmin age1newadmin...

creation_rules:
  - path_regex: secrets/.*\.yaml$
    key_groups:
      - age:
          - *server
          - *backup
          - *newadmin
}}}

3. Rotate all secrets:

# sops automatically re-encrypts with all recipients

sops rotate -i secrets/*.yaml

}}}

Removing a Recipient

1. Remove key from .sops.yaml

2. Rotate all secrets (they'll be encrypted for remaining recipients only)

Rotating Secrets

Rotating Data Keys

This re-encrypts file without changing secret values:

sops rotate -i secrets/bluesky-pds.yaml
}}}

Do this when:
- Adding/removing recipients
- You suspect key compromise
- Periodic security maintenance (monthly/quarterly)

=== Rotating Secret Values ===

This changes actual secret (e.g., password, API key):

# 1. Edit and change values

sops secrets/bluesky-pds.yaml

# 2. Deploy

sudo nixos-rebuild switch --flake .#snek

# 3. Restart affected services

sudo systemctl restart bluesky-pds

}}}

Complete Rotation Checklist

# 1. Generate new secrets
echo "PDS_JWT_SECRET: $(openssl rand -hex 32)" > /tmp/new-secrets.txt

# 2. Edit encrypted file with new values
sops secrets/bluesky-pds.yaml
# (copy-paste new values)

# 3. Rotate encryption keys
sops rotate -i secrets/bluesky-pds.yaml

# 4. Commit changes
git add secrets/
git commit -m "Rotate PDS secrets"

# 5. Deploy
sudo nixos-rebuild switch --flake .#snek

# 6. Restart services
sudo systemctl restart bluesky-pds

# 7. Verify
systemctl status bluesky-pds
journalctl -u bluesky-pds -f
}}}

== Backup and Recovery ==

=== What to Backup ===

*Critical:*
- `/etc/nixos/secrets/*.yaml` (encrypted secrets)
- `/etc/nixos/secrets/.sops.yaml` (encryption config)
- Age secret keys (if using file-based keys)

*How to backup:*

# The secrets are already encrypted, safe to commit to git

cd /etc/nixos

git add secrets/

git commit -m "Backup secrets"

git push

}}}

Recovery Scenarios

Scenario 1: Lost Server, Have Secrets Repository

1. Provision new server

2. Install NixOS

3. Clone your config repository (includes encrypted secrets)

4. Get new server's SSH key

5. Add as recipient to .sops.yaml

6. Rotate secrets to include new recipient

7. Deploy

Scenario 2: Forgot to Backup .sops.yaml

If you have to secrets files but lost .sops.yaml:

# Check if you can still decrypt
sops -d secrets/bluesky-pds.yaml

# If yes, recreate .sops.yaml with current recipients
# (sops stores recipient info in file itself)
}}}

*Scenario 3: Lost All Age Keys*

Worst case - you can't decrypt secrets anymore:

1. Generate new secrets from scratch
2. Update services with new secrets
3. Revoke old API keys/tokens if possible

*Prevention:*
- Always have multiple recipients (backup key stored offline)
- Commit .sops.yaml to git
- Backup age secret keys to secure offline storage

=== Testing Recovery ===

Test your recovery process before you need it:

# 1. Clone your config to a temporary directory

cd /tmp

git clone https://github.com/your/config.git test-recovery

cd test-recovery

# 2. Try to decrypt secrets

sops -d secrets/bluesky-pds.yaml

# 3. If this works, your backup is good

# If not, fix your backup process

}}}

Best Practices

1. *Never commit unencrypted secrets* - Always use sops

2. *Use multiple recipients* - Backup key stored securely offline

3. *Rotate regularly* - Monthly for critical secrets

4. *Test recovery* - Verify you can decrypt from backup

5. *Document what each secret is for* - Add comments in YAML

6. *Use strong passwords* - For any password-type secrets

7. *Separate secrets by service* - Don't put everything in one file

8. *Version control* - Commit encrypted files to track changes

Troubleshooting

"cannot decrypt data key: no key found"

"permission denied" reading /run/secrets/

"failed to get plain text data key"

Secrets not updating after edit

References