KAHIBARO
Discord Login Register

23.6. Managing Secrets

Why Secrets Matter in Deployment

In a backend application you always have sensitive values, for example:

All of these are secrets. If someone gets them, they can:

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.

TypeExampleSecret?
Database passwordDB_PASSWORD=sup3rS3cret!Yes
Database hostDB_HOST=db.example.comNo
API keySTRIPE_SECRET_KEY=sk_live_...Yes
Port numberPORT=8000No
JWT signing keyJWT_SECRET=long_random_stringYes
Feature flagENABLE_NEW_UI=trueNo
S3 access key & secretAWS_ACCESS_KEY_ID, AWS_SECRET_KEYYes

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):

python
# config.py
DB_PASSWORD = "supersecretpassword"  # ❌ bad
JWT_SECRET = "myjwtsecretkey"        # ❌ bad

Why this is dangerous:

Committing `.env` files to Git

You might use a .env file for local development:

env
DB_USER=myuser
DB_PASSWORD=mydevsecret

This is fine for local, but it is often accidentally committed.

Add .env to .gitignore:

gitignore
.env
.env.*

Then check:

bash
git status
git diff

to ensure nothing sensitive is staged.

Sharing secrets in plain text

Avoid sending secrets through:

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:

Instead, create a dedicated DB user with only the permissions your app actually needs.

Separation of environments

Use different secrets for different environments:

Never reuse production secrets anywhere else.

Rotating secrets

Secrets will eventually:

So you must be able to rotate them:

  1. Add a new secret.
  2. Update your app to use the new secret.
  3. 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:

python
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:

bash
sudo nano /etc/myapp/env

Content:

env
DATABASE_URL=postgresql://user:pass@db:5432/app
JWT_SECRET=super_long_random_secret

Make it readable only by root:

bash
sudo chmod 600 /etc/myapp/env

Example systemd service file /etc/systemd/system/myapp.service:

ini
[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:

yaml
environment:
  - DATABASE_URL=postgresql://user:secret@db:5432/app  # ❌

Better, reference a .env that is not committed to Git:

yaml
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:

bash
docker run -e DATABASE_URL=... -e JWT_SECRET=... myapp:latest

Secret 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:

  1. Go to Settings > Secrets and variables > Actions.
  2. Create secrets like PROD_DATABASE_URL, PROD_JWT_SECRET.
  3. Use them in your workflow:
yaml
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:

Again, store these in your CI secret storage, not in config files.

Example: Platform‑specific secret stores

Most major providers have a secret manager:

ProviderService name
AWSAWS Secrets Manager, SSM
Google CloudSecret Manager
AzureKey Vault
HashiCorpVault

Common pattern:

  1. Store secrets in the secret manager.
  2. Grant your app permission to access them.
  3. 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:

Basic rules:

Example GitHub Actions snippet:

yaml
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:

Example .env.example:

env
# Example config, do not use in production
DATABASE_URL=postgresql://user:password@localhost:5432/app
JWT_SECRET=change_me
REDIS_URL=redis://localhost:6379/0

Developer copies it:

bash
cp .env.example .env.local

Then edits values. .env.local is ignored by Git.

On the server, you might have /opt/myapp/.env.prod:

env
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:

yaml
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:

  1. Rotate the secret immediately (change database password, regenerate API key).
  2. Assume the old secret is compromised forever.
  3. 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:

python
print(f"Connecting to database at {DATABASE_URL}")  # may contain secret

Instead, log a safe version:

python
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:

If you can check all of these, your deployment setup is in good shape for managing secrets.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!