Skip to main content

Secrets & .env β€” NHS Guide

πŸ” Key Vault / Secrets Manager Β· .env for dev Β· Env vars Β· CI/CD Β· Docker/Kubernetes
Goals
  • Keep credentials out of git.
  • Make local dev easy with .env, but use a secret store in staging/prod.
  • Standardise env var names so examples and CI pipelines interoperate.

🧭 Naming convention (cross-language)​

Use these keys in examples, CI, and containers:

SQLSERVER_SERVER=server_or_fqdn_or_dsn
SQLSERVER_DATABASE=NHS_Analytics
SQLSERVER_USER=svc_ds # omit when using Integrated Auth / Managed Identity
SQLSERVER_PASSWORD=******** # omit when using Integrated Auth / Managed Identity
API_KEY=dev-local-key # dev only
VITE_API_URL=http://localhost:8000 # front-ends only

Add .env to .gitignore in every repo.


πŸ’» Local development (.env)​

Load variables with python-dotenv.

pip install python-dotenv
# settings.py
from dotenv import load_dotenv; load_dotenv()
import os

SQLSERVER_SERVER = os.getenv("SQLSERVER_SERVER", "")
SQLSERVER_DATABASE= os.getenv("SQLSERVER_DATABASE", "")

Do not commit .env. Use synthetic data in repos.


πŸ” Production secret stores​

Best fit when hosting on Azure App Service / Container Apps. Assign a System-Assigned Managed Identity, grant vault access, fetch at runtime.

# create vault & secret (CLI sketch)
az keyvault create -g rg-nhs -n nhs-secrets
az keyvault secret set --vault-name nhs-secrets -n SqlPassword --value "StrongPass123!"
# Python (FastAPI/Dash) β€” fetch once at startup
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
import os

VAULT_URL = os.getenv("KEYVAULT_URL") # e.g., https://nhs-secrets.vault.azure.net/
client = SecretClient(vault_url=VAULT_URL, credential=DefaultAzureCredential())
SQL_PASSWORD = client.get_secret("SqlPassword").value
// Node (Express/Next.js) β€” server-side only
const { DefaultAzureCredential } = require("@azure/identity")
const { SecretClient } = require("@azure/keyvault-secrets")
const cred = new DefaultAzureCredential()
const client = new SecretClient(process.env.KEYVAULT_URL, cred)
const { value: SQL_PASSWORD } = await client.getSecret("SqlPassword")

Pattern: fetch once at startup β†’ cache in memory β†’ never log values.


πŸ“¦ Containers & runtime config​

  • Docker run: --env-file .env for local, never in prod images.
  • App Runner / Azure Apps: set environment variables in the platform UI or IaC; reference secrets from the store.
  • 12‑factor: code reads config from environment at startup.

Docker Compose (dev)

services:
api:
build: .
env_file: .env
ports: ["8000:8000"]

πŸ€– CI/CD (GitHub Actions)​

  • Keep secrets in Settings β†’ Secrets and variables β†’ Actions.
  • Prefer OIDC federation (short‑lived creds) over stored cloud keys.
.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use secrets safely
env:
API_URL: ${{ secrets.API_URL }}
run: |
echo "Hitting masked endpoint"
curl -sS "$API_URL/healthz" || true

Masking: GitHub auto‑masks ${{ secrets.* }} in logs. Avoid echo‑ing secret values directly.


❌ Patterns to avoid​

  • Baking secrets into images or source control.
  • Passing secrets on the command line (visible via ps/history).
  • Logging full connection strings or tokens.
  • Client-side React envs for secrets (anything NEXT_PUBLIC_ or VITE_ is public).

βœ… IG & safety checklist​

  • .env in .gitignore; repos use synthetic data only.
  • Production uses Key Vault / Secrets Manager; rotation documented.
  • TLS/Encrypt enforced in connection strings.
  • Small-number suppression happens server-side.
  • Principle of least privilege for app identities/roles.

πŸ“ Measuring impact​

  • Zero leaked secrets (gitleaks clean).
  • Mean time to rotate credentials after a change.
  • % services using secret stores vs files.
  • Audit completeness: where a secret comes from, who owns it.

πŸ”— See also​

See also: FastAPI Β· Express.js Β· Docker Β· AWS Β· Azure Β· GitHub Β· SQL

What’s next?

You’ve completed the Learn β€” Secrets stage. Keep momentum: