mirror of
https://github.com/RWejlgaard/pez-infra.git
synced 2026-05-06 04:14:43 +00:00
152 lines
4.9 KiB
Markdown
152 lines
4.9 KiB
Markdown
# Secrets Management
|
|
|
|
This repo uses [SOPS](https://github.com/getsops/sops) with [age](https://github.com/FiloSottile/age) encryption for secrets. Encrypted files live in the repo alongside the configs they belong to — only the secret values are encrypted, so diffs remain useful.
|
|
|
|
## Why SOPS + age?
|
|
|
|
- **age over GPG**: No key expiry, no keyservers, no UID headaches. A single static public key per recipient.
|
|
- **SOPS over git-crypt**: Encrypts values, not whole files. You can see the structure of a secrets file without decrypting it. Works with YAML, JSON, ENV, and INI.
|
|
- **SOPS over Ansible Vault**: Ansible Vault only works with Ansible. SOPS works everywhere — Terraform (via `terraform-provider-sops`), Docker env files, CI pipelines, scripts.
|
|
|
|
## File naming convention
|
|
|
|
Encrypted files use `.enc.` in their extension:
|
|
|
|
```
|
|
services/authelia/config.enc.yml # encrypted YAML
|
|
services/miniflux/miniflux.enc.env # encrypted env file
|
|
terraform/secrets.enc.yaml # encrypted Terraform vars
|
|
ansible/group_vars/all/secrets.enc.yml
|
|
```
|
|
|
|
Plaintext files MUST NOT contain secrets. The `.gitignore` blocks common secret filenames (`secrets.yml`, `vault.yml`, `secret.env`, etc.) as a safety net.
|
|
|
|
## Setup (one-time)
|
|
|
|
### Install tools
|
|
|
|
```bash
|
|
# macOS
|
|
brew install sops age
|
|
|
|
# Debian/Ubuntu
|
|
apt install age
|
|
# SOPS: download from https://github.com/getsops/sops/releases
|
|
wget https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb
|
|
dpkg -i sops_3.9.4_amd64.deb
|
|
|
|
# FreeBSD
|
|
pkg install age sops
|
|
```
|
|
|
|
### Generate your age key
|
|
|
|
```bash
|
|
age-keygen -o ~/.config/sops/age/keys.txt
|
|
# Output: public key: age1abc123...
|
|
```
|
|
|
|
This file is your private key. **Never commit it.** The `.gitignore` already blocks `keys.txt` and `*.agekey`.
|
|
|
|
SOPS automatically looks for keys in `~/.config/sops/age/keys.txt` (Linux/macOS) or you can set `SOPS_AGE_KEY_FILE` to point elsewhere.
|
|
|
|
### Add your public key to `.sops.yaml`
|
|
|
|
Replace the `age1TODO_PEZ_PUBLIC_KEY` placeholder in `.sops.yaml` with your actual public key. Commit the updated `.sops.yaml`.
|
|
|
|
## Day-to-day usage
|
|
|
|
### Create a new encrypted file
|
|
|
|
```bash
|
|
# SOPS picks the right age keys from .sops.yaml based on file path
|
|
sops services/authelia/config.enc.yml
|
|
# Opens your $EDITOR with a decrypted view. Save and quit to encrypt.
|
|
```
|
|
|
|
### Edit an existing encrypted file
|
|
|
|
```bash
|
|
sops services/authelia/config.enc.yml
|
|
```
|
|
|
|
### Decrypt to stdout (for scripts/debugging)
|
|
|
|
```bash
|
|
sops -d services/authelia/config.enc.yml
|
|
```
|
|
|
|
### Encrypt an existing plaintext file
|
|
|
|
```bash
|
|
# If you have a plaintext file you want to encrypt in-place:
|
|
sops -e -i services/miniflux/miniflux.enc.env
|
|
```
|
|
|
|
### Add a new recipient
|
|
|
|
When someone new needs access (or a new CI key is generated):
|
|
|
|
1. Get their age public key
|
|
2. Add it to the relevant `creation_rules` in `.sops.yaml`
|
|
3. Re-encrypt all affected files:
|
|
|
|
```bash
|
|
# Update keys on all encrypted files
|
|
find . -name '*.enc.*' -exec sops updatekeys {} \;
|
|
```
|
|
|
|
## CI / GitHub Actions
|
|
|
|
The CI runner needs to decrypt secrets during deploys. Store the age secret key as a GitHub Actions secret:
|
|
|
|
1. Generate a CI-specific age key: `age-keygen`
|
|
2. Add the **private key** (the `AGE-SECRET-KEY-1...` line) as a GitHub repository secret named `AGE_SECRET_KEY`
|
|
3. Add the **public key** to `.sops.yaml` (the CI recipient)
|
|
|
|
In the workflow:
|
|
|
|
```yaml
|
|
- name: Decrypt secrets
|
|
env:
|
|
SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
run: |
|
|
sops -d ansible/group_vars/all/secrets.enc.yml > ansible/group_vars/all/secrets.yml
|
|
```
|
|
|
|
The existing `ANSIBLE_VAULT_PASS` secret can be retired once migration to SOPS is complete.
|
|
|
|
## Terraform integration
|
|
|
|
Use the [terraform-provider-sops](https://github.com/carlpett/terraform-provider-sops) to read encrypted values directly:
|
|
|
|
```hcl
|
|
provider "sops" {}
|
|
|
|
data "sops_file" "secrets" {
|
|
source_file = "secrets.enc.yaml"
|
|
}
|
|
|
|
# Use decrypted values
|
|
resource "cloudflare_record" "example" {
|
|
value = data.sops_file.secrets.data["cloudflare_api_token"]
|
|
}
|
|
```
|
|
|
|
## What gets encrypted
|
|
|
|
These are the types of secrets expected in this repo:
|
|
|
|
| Category | Example | Location |
|
|
|----------|---------|----------|
|
|
| Ansible vault vars | SSH keys, API tokens, passwords | `ansible/group_vars/*/secrets.enc.yml` |
|
|
| Docker env files | DB passwords, app secrets | `services/*/service.enc.env` |
|
|
| Terraform vars | Cloudflare API token, Azure creds | `terraform/secrets.enc.yaml` |
|
|
| Service configs | Authelia JWT secret, LLDAP admin pass | `services/*/config.enc.yml` |
|
|
|
|
## Security notes
|
|
|
|
- **Never commit `keys.txt`** or any file containing `AGE-SECRET-KEY`. The `.gitignore` blocks these.
|
|
- **Rotate keys** if a machine is compromised: generate new key, update `.sops.yaml`, re-encrypt all files, revoke the old key from `.sops.yaml`.
|
|
- **CI key is separate** from personal keys so it can be rotated independently.
|
|
- SOPS encrypted files contain metadata about which keys can decrypt them — this is intentional and not a secret.
|