23.6. Managing Secrets
Table of Contents
Why Secrets Matter in Deployment
In a backend application you always have sensitive values, for example:
- Database passwords
- API keys for third‑party services (Stripe, SendGrid, etc.)
- JWT signing keys
- OAuth client secrets
- Encryption keys
All of these are secrets. If someone gets them, they can:
- Read or delete your database
- Send emails as your app
- Charge credit cards through your payment provider
- Fake valid authentication tokens
So managing secrets safely is a core part of deploying to production.
Important rule:
Never hard‑code secrets in your code or commit them to Git.
In this chapter we focus on what is specific to managing secrets at deployment time, not on how encryption or authentication work internally.
What Counts as a Secret?
Not every configuration value is a secret.
| Type | Example | Secret? |
|---|---|---|
| Database password | DB_PASSWORD=sup3rS3cret! | Yes |
| Database host | DB_HOST=db.example.com | No |
| API key | STRIPE_SECRET_KEY=sk_live_... | Yes |
| Port number | PORT=8000 | No |
| JWT signing key | JWT_SECRET=long_random_string | Yes |
| Feature flag | ENABLE_NEW_UI=true | No |
| S3 access key & secret | AWS_ACCESS_KEY_ID, AWS_SECRET_KEY | Yes |
A good test: If someone else had this value, could they impersonate your app or access private data? If yes, treat it as a secret.
Bad Ways to Handle Secrets
Before good practices, here are patterns you must avoid.
Hard‑coding secrets in source code
Example (Python):
# config.py
DB_PASSWORD = "supersecretpassword" # ❌ bad
JWT_SECRET = "myjwtsecretkey" # ❌ badWhy this is dangerous:
- It is very easy to accidentally commit to Git.
- Everyone with repo access can see the secrets.
- If your code is ever made public, your secrets are exposed.
- Rotating (changing) the secret means changing code and redeploying.
Committing `.env` files to Git
You might use a .env file for local development:
DB_USER=myuser
DB_PASSWORD=mydevsecretThis is fine for local, but it is often accidentally committed.
Add .env to .gitignore:
.env
.env.*Then check:
git status
git diffto ensure nothing sensitive is staged.
Sharing secrets in plain text
Avoid sending secrets through:
- Public channels (Slack, Discord, forums)
- Unencrypted emails or screenshots
- Pastebin or similar public tools
Instead, use secure channels or a proper secret manager.
Principles of Secure Secret Management
Least privilege
Each secret should give the minimum access required.
Example:
Database user for the app should not be able to:
- Create new superusers
- Drop the entire database (unless absolutely needed)
Instead, create a dedicated DB user with only the permissions your app actually needs.
Separation of environments
Use different secrets for different environments:
DB_PASSWORD_DEVfor developmentDB_PASSWORD_STAGINGfor stagingDB_PASSWORD_PRODfor production
Never reuse production secrets anywhere else.
Rotating secrets
Secrets will eventually:
- Leak
- Be shared with someone who should no longer have access
- Need to be updated for compliance
So you must be able to rotate them:
- Add a new secret.
- Update your app to use the new secret.
- Remove the old secret.
If your deployment setup makes this hard, fix that early.
Environment Variables and Deployment
Environment variables are the most common way to pass secrets to your app in production.
Pattern: configuration via environment variables
In your code:
import os
DATABASE_URL = os.getenv("DATABASE_URL")
JWT_SECRET = os.getenv("JWT_SECRET")At deployment time you set the environment variables, not in code.
Important rule:
Code should read secrets from the environment, not define them.
This pattern is known as 12‑factor app configuration.
Setting env vars on a Linux server
If you deploy directly to a Linux server (without containers), use secure files and a system service.
Example secret file owned by root:
sudo nano /etc/myapp/envContent:
DATABASE_URL=postgresql://user:pass@db:5432/app
JWT_SECRET=super_long_random_secretMake it readable only by root:
sudo chmod 600 /etc/myapp/env
Example systemd service file /etc/systemd/system/myapp.service:
[Service]
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/bin/python -m myapp.main
User=myapp
Group=myapp
systemd will load env vars from that file without exposing them in your code.
Setting env vars with Docker
If you use Docker, you often configure secrets through environment variables in docker-compose.yml or the deployment platform.
Avoid hard‑coding secrets in docker-compose.yml, especially if that file is committed.
Bad:
environment:
- DATABASE_URL=postgresql://user:secret@db:5432/app # ❌
Better, reference a .env that is not committed to Git:
env_file:
- .env.prod
Then .env.prod stays outside the repo or is stored in a secure place.
You can also pass variables at run time:
docker run -e DATABASE_URL=... -e JWT_SECRET=... myapp:latestSecret Management in Cloud Platforms
Most cloud platforms offer a secret storage feature. Use these instead of manually editing files where possible.
Example: GitHub Actions secrets
For CI/CD pipelines you must avoid committing secrets in workflow files.
Instead, configure them in your repository:
- Go to Settings > Secrets and variables > Actions.
- Create secrets like
PROD_DATABASE_URL,PROD_JWT_SECRET. - Use them in your workflow:
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
JWT_SECRET: ${{ secrets.PROD_JWT_SECRET }}Secrets are masked in logs.
Example: Docker registry credentials
To push images to a private registry you often set secrets like:
DOCKER_USERNAMEDOCKER_PASSWORD
Again, store these in your CI secret storage, not in config files.
Example: Platform‑specific secret stores
Most major providers have a secret manager:
| Provider | Service name |
|---|---|
| AWS | AWS Secrets Manager, SSM |
| Google Cloud | Secret Manager |
| Azure | Key Vault |
| HashiCorp | Vault |
Common pattern:
- Store secrets in the secret manager.
- Grant your app permission to access them.
- On startup, app reads them into environment variables or memory.
You usually do not need to implement this yourself at first, but know that this is the standard for larger systems.
Handling Secrets in CI/CD Pipelines
CI/CD systems need some secrets:
- To access your server (SSH keys)
- To push to container registries
- To connect to test or staging databases
- To sign artifacts
Basic rules:
- Use the CI platform’s encrypted secret storage feature.
- Inject them as environment variables at build or deploy steps.
- Never print them in logs.
- Use separate, limited credentials for CI (least privilege).
Example GitHub Actions snippet:
jobs:
deploy:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
JWT_SECRET: ${{ secrets.STAGING_JWT_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
docker login -u "${{ secrets.REGISTRY_USER }}" \
-p "${{ secrets.REGISTRY_PASSWORD }}" registry.example.com
# deploy commands...Practical Project Setup for Secrets
For a small FastAPI + PostgreSQL project deployed with Docker, a practical structure is:
.env.example(committed).env.local(local only).env.prod(kept outside repo, used by server or CI)
Example .env.example:
# Example config, do not use in production
DATABASE_URL=postgresql://user:password@localhost:5432/app
JWT_SECRET=change_me
REDIS_URL=redis://localhost:6379/0Developer copies it:
cp .env.example .env.local
Then edits values. .env.local is ignored by Git.
On the server, you might have /opt/myapp/.env.prod:
DATABASE_URL=postgresql://prod_user:prod_pass@db:5432/app
JWT_SECRET=very_long_random_string
REDIS_URL=redis://redis:6379/0
And docker-compose.yml:
services:
api:
image: myapp:latest
env_file:
- .env.prod
docker-compose.yml can be in Git, .env.prod is not.
Common Mistakes and How to Avoid Them
Checking secrets into Git, then deleting them
Even if you remove a secret from a file, Git history still has it.
If this happens:
- Rotate the secret immediately (change database password, regenerate API key).
- Assume the old secret is compromised forever.
- Optionally, clean Git history with tools like
git filter-repo, but that does not undo exposure.
Important rule:
If a secret was ever pushed to a remote repository, treat it as leaked and rotate it.
History cleanup does not make the secret safe again.
Using the same secret everywhere
Do not use one JWT secret for dev, staging, and prod. If dev is compromised, prod is also compromised.
Use separate secrets for each environment.
Logging secrets
Do not log full database URLs with passwords, or request bodies that may contain passwords or API keys.
Example to avoid:
print(f"Connecting to database at {DATABASE_URL}") # may contain secretInstead, log a safe version:
from urllib.parse import urlparse
parsed = urlparse(DATABASE_URL)
safe = f"{parsed.scheme}://{parsed.hostname}:{parsed.port}/{parsed.path.lstrip('/')}"
print(f"Connecting to database at {safe}")Checklist for Managing Secrets in Production
Use this as a quick reference when preparing deployments:
- [ ] No secrets are hard‑coded in source files.
- [ ]
.envand secret files are in.gitignore. - [ ] Different secrets per environment (dev, staging, prod).
- [ ] Secrets are provided to the app via environment variables or a secret manager.
- [ ] Production secret files (if any) have strict permissions.
- [ ] CI/CD uses encrypted secret storage, not plain text.
- [ ] Logs and error messages do not print secrets.
- [ ] You have a clear way to rotate secrets without code changes.
If you can check all of these, your deployment setup is in good shape for managing secrets.
Views: 8
KAHIBARO