Secrets management
Where secrets live
| Secret | Dev | Prod | Shared with developers via |
|---|---|---|---|
Backend .env (database, salt, Stripe, Deposco, ACS, Graph, Google, R2, Maps, OpenAI, tunnel token) | /opt/emaw/emaw-django-app/.env on the dev VM | Portainer stack environment | Password manager (never git, chat or email) |
| Production username salt | n/a (EMAW_SALT in .env) | Key Vault emaw-app, secret emaw-app-salt | not shared; the VM's identity reads it |
| Cloudflare API token + account id | GitHub environment develop in each frontend repo | GitHub environment production | Cloudflare account owner |
Website VITE_* (public) | GitHub repository variables | Pages production variables | not secret |
Web app / CRM / mobile VITE_* and EXPO_PUBLIC_* (public) | committed env files | committed env files | not secret |
| Stripe dashboard login | Stripe team invite | ||
| Apple / Google developer accounts, EAS | Store console roles; Expo organisation | ||
| SSH keys | your own private key; public key installed by Troy or Matt | same + IP whitelist |
Rule of thumb: if it is a VITE_ or EXPO_PUBLIC_ value, it ships to browsers and phones and is not a secret; if it is anything else, it belongs only in a backend .env, Portainer, Key Vault or a GitHub secret.
Getting a secret into an environment
| Environment | How |
|---|---|
| Your laptop (backend) | copy the .env from the password manager into the repo root; it is gitignored |
| Dev VM | edit /opt/emaw/emaw-django-app/.env over SSH, then docker compose up -d |
| Production | Portainer → stack → environment variables, then redeploy. Also add the name to docker-compose.yml x-app-env or the container will not see it |
| GitHub Actions | repo → Settings → Environments → develop / production → environment secrets |
| Cloudflare Pages (website) | project → Settings → Environment variables → Production |
Rotation notes
| Secret | Effect of rotating | Steps |
|---|---|---|
SECRET_KEY | Invalidates all JWTs (everyone signs in again) and GOOGLE_TOKEN_KEY's default, so staff must reconnect Google for Meet | Set an explicit GOOGLE_TOKEN_KEY first if you want to keep Meet connections |
EMAW_SALT | Nobody can log in: usernames are salted hashes | Do not rotate; it is effectively permanent |
STRIPE_WEBHOOK_SECRET | Webhooks fail signature checks until updated | Roll the secret in Stripe, update .env, redeploy quickly |
MS_CLIENT_SECRET | Expires on a schedule in Entra; emails stop silently | Create a new secret before expiry, update both environments |
TUNNEL_TOKEN | The backend disappears from the internet until the new token runs | Rotate in Zero Trust, update .env on the VM and in Portainer |
R2_* token | Uploads return 503 | Create a new token, update .env |
| Cloudflare API token (GitHub) | Deploys fail | Create a new token, update the environment secrets in every frontend repo |
AZURE_BLOB_KEY | Media uploads and reads of generated audio fail | Regenerate in the storage account, update both environments |
If a secret leaks
- Rotate it immediately using the table above.
- Tell Troy or Matt directly; do not post the value or details in a shared channel.
- If it was committed, rewrite history only after rotating; rotation is what fixes the exposure.