Aller au contenu

Environment Management

Overview

Every environment file on the server is encrypted with SOPS + age and stored in the aether repo as .enc. The plaintext versions live only on the server and are gitignored. Workflow:

┌─────────────────────┐                           ┌──────────────────────┐
│  aether repo (git)  │  ── make decrypt ────▶    │  /opt/docker/*/...   │
│  *.enc files        │  ◀── make encrypt ────    │  plaintext files     │
└─────────────────────┘                           └──────────────────────┘

Golden rule: never commit a plaintext env file. If you accidentally do, rotate every secret in that file — the history is permanent.

Managed env files

Encrypted file (repo) Decrypted location (server)
shared/.env.enc /opt/docker/shared/.env
envs/aletheia/.env.dev.enc /opt/docker/aletheia/envs/.env.dev
envs/aletheia/.env.staging.enc /opt/docker/aletheia/envs/.env.staging
envs/aletheia/.env.prod.enc /opt/docker/aletheia/envs/.env.prod
envs/helios/.env.dev.enc /opt/docker/helios/envs/.env.dev
envs/helios/.env.staging.enc /opt/docker/helios/envs/.env.staging
envs/helios/.env.prod.enc /opt/docker/helios/envs/.env.prod
monitoring/.env.enc /opt/docker/monitoring/.env
umami/.env.enc /opt/docker/umami/.env
nginx/.htpasswd.enc /opt/docker/nginx/.htpasswd
shared/init-scripts/01-create-databases.sql.enc /opt/docker/shared/init-scripts/01-create-databases.sql

Total: 11 encrypted file(s) managed via make encrypt / make decrypt.

The age private key lives at /opt/docker/.age-key.txt (640 root:debian). It's NOT stored in the repo — losing it means losing access to every secret. Keep a copy in the team password manager.

The three operations

make decrypt — server ← repo

Decrypts every .enc file in the repo into its server location. Used when:

  • Setting up a new server (called automatically by setup.sh --recover)
  • Pulling a fresh server state after another team member rotated secrets
  • After losing local plaintext (accidental delete)

Requires: age private key at /opt/docker/.age-key.txt.

make encrypt — repo ← server

Encrypts every plaintext env file on the server back into .enc in the repo. Used when:

  • You've edited a secret on the server and want to persist the change
  • After rotating a password
  • After helios-init or umami-init generated new secrets

Encrypt fails silently if a source file is missing

make encrypt skips files that don't exist on the server with a warning, not an error. If you just deleted a decrypted file and want to make sure the .enc still has good content, check git diff — an empty or truncated .enc means encryption read a missing or empty source.

make diff — detect drift

Compares every tracked config file (and by implication the .enc files through the encrypt/decrypt cycle) between repo and server. Use before every deploy.

The standard edit cycle

Any time you need to change a secret value (password rotation, new API key, SMTP change, etc.):

# 1. Edit the plaintext file on the server
sudo nano /opt/docker/aletheia/envs/.env.prod

# 2. If this is a password that's stored in a remote service too
#    (e.g. PostgreSQL user password), update both ends:
docker exec shared_postgres psql -U admin -c \
  "ALTER USER aletheia_prod WITH PASSWORD 'new-value';"

# 3. Restart the affected service to pick up the new value
cd /opt/docker/aletheia/repo && make restart ENV=prod

# 4. Verify the service is healthy
curl -sI https://aletheia.groupe-suffren.com/health/

# 5. Encrypt the change back into the repo
cd /opt/docker/aether/repo && make encrypt

# 6. Commit and push
git diff envs/aletheia/.env.prod.enc     # review (encrypted noise, but timestamp changes)
git add envs/aletheia/.env.prod.enc
git commit -m "Rotate aletheia prod <credential name>"
git push

Always restart and verify before encrypting

Step 5 (encrypt) captures whatever state is on disk. If your new password breaks the service and you haven't encrypted yet, the old plaintext is still on the server and you can revert. Once encrypted, the new value is what make decrypt restores — rollback requires git revert.

Per-app environment reference

The authoritative env variable list for each app lives with that app's code, not here. This section gives pointers.

Aletheia (envs/aletheia/.env.{dev,staging,prod})

Django settings, Celery broker, DB credentials, integrations (Brevo, Sentry, Pennylane, Doctolib). Reference: aletheia/repo/.env.example + settings modules under aletheia/repo/config/settings/.

Key vars (high-level):

  • POSTGRES_* — database credentials (user per environment)
  • REDIS_URL, CACHE_REDIS_URLredis://shared_redis:6379/<DB> (DB 0/1 prod, 2/3 staging, 4/5 dev — see shared services)
  • DJANGO_SECRET_KEY — must be unique per environment, generated at setup time
  • SENTRY_DSN — per-environment Sentry project
  • BREVO_API_KEY — email/marketing
  • HELIOS_REVALIDATION_SECRET — shared secret for ISR webhooks (must match the corresponding Helios env file)

Helios (envs/helios/.env.{dev,staging,prod})

See Helios for the full breakdown.

Shared services (shared/.env)

Single file, shared across environments.

  • POSTGRES_USER=admin — superuser account
  • POSTGRES_PASSWORD=... — superuser password (only used for admin operations)

Individual app DB users are created by the init SQL script (shared/init-scripts/01-create-databases.sql.enc).

Monitoring (monitoring/.env)

  • GF_SECURITY_ADMIN_PASSWORD — Grafana admin login
  • POSTGRES_MONITORING_USER, POSTGRES_MONITORING_PASSWORD — readonly user for postgres-exporter
  • SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_ADDRESS — Brevo relay for alert emails
  • TEAMS_WEBHOOK_URL — Power Automate HTTP trigger (see alerting)

Umami (umami/.env)

  • UMAMI_DB_USER, UMAMI_DB_PASSWORD, UMAMI_DB_NAME — Umami's PostgreSQL credentials
  • APP_SECRET — session signing key

Nginx basic auth (nginx/.htpasswd)

Not an env file but managed the same way. Controls access to staging and dev vhosts. To add a user: htpasswd /opt/docker/nginx/.htpasswd <username>, then make encrypt.

Adding a new environment

If you need a 4th environment (e.g. qa, demo, preprod):

  1. Create the plaintext env file on the server:
    sudo cp /opt/docker/aletheia/envs/.env.staging /opt/docker/aletheia/envs/.env.qa
    sudo nano /opt/docker/aletheia/envs/.env.qa   # customize
    
  2. Register it in the Makefile — add to SECRET_FILES:
    envs/aletheia/.env.qa:aletheia/envs/.env.qa \
    
  3. Create the database on PostgreSQL (see shared services)
  4. Add nginx vhost (nginx/conf.d/aletheia-qa.conf.full + .conf.temp)
  5. Obtain SSL cert for the new domain
  6. Add to INFRA_FILES in the Makefile so diff/deploy track the new nginx configs
  7. Deploy: make deploy && make restart
  8. Encrypt: make encrypt && git commit

Adding a new secret file

If you add a new service that needs encrypted env (e.g. a new app):

  1. Create the plaintext file on the server, populate it
  2. Add an entry to SECRET_FILES in the Makefile:
    myservice/.env:myservice/.env \
    
  3. Run make encrypt — the new .enc file appears in the repo
  4. Add myservice/.env to .gitignore if not already covered
  5. Commit the .enc file and Makefile change

Lost age key recovery

If /opt/docker/.age-key.txt is lost:

  1. Retrieve the age private key from the team password manager
  2. Write it back: sudo tee /opt/docker/.age-key.txt <<< 'AGE-SECRET-KEY-...'
  3. Set permissions: sudo chmod 640 /opt/docker/.age-key.txt && sudo chown root:debian /opt/docker/.age-key.txt
  4. Run make decrypt to restore all secrets

If the key is truly lost (not in the password manager either), every .enc file is unrecoverable. You'd need to regenerate every password:

  1. Generate a new age keypair: age-keygen -o /opt/docker/.age-key.txt
  2. Update the recipient in .sops.yaml (in the repo root) to the new public key
  3. Manually populate every plaintext env file with fresh values (new passwords, re-issued API keys, etc.)
  4. Run make encrypt to create new .enc files
  5. Update the PostgreSQL user passwords to match (they won't match the old dump if you restore from a backup that predates this)
  6. Save the new age key to the password manager

Avoid this at all costs. Put the age key in your password manager the day you generate it.

Verification

After any secret change, sanity-check:

# 1. Plaintext file exists and is readable
sudo ls -la /opt/docker/<path>/.env

# 2. Service that uses it is healthy
docker ps --filter name=<container>

# 3. Encrypted file committed and pushed
cd /opt/docker/aether/repo
git status && git log -1 envs/<app>/.env.<env>.enc

# 4. Deploy matches git
make diff