Secrets & .env β NHS Guide
- 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)β
- Python
- Node.js
- R
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", "")
Use dotenv early in your app.
npm i dotenv
// index.js
require('dotenv').config()
const server = process.env.SQLSERVER_SERVER
Load with dotenv or config.
install.packages("dotenv")
library(dotenv)
load_dot_env()
Sys.getenv("SQLSERVER_SERVER")
Do not commit .env. Use synthetic data in repos.
π Production secret storesβ
- Azure Key Vault + Managed Identity
- AWS Secrets Manager + IAM role
- Kubernetes (AKS/EKS) Secrets
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")
Use on AWS App Runner/ECS/Lambda. Grant the task/service role secretsmanager:GetSecretValue, then fetch at runtime or inject as env.
# attach policy to role (conceptual)
aws iam attach-role-policy --role-name app-role --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite
# Python β boto3 fetch (cache after first read)
import boto3, os, json
arn = os.getenv("SECRET_ARN")
sm = boto3.client("secretsmanager")
secret = json.loads(sm.get_secret_value(SecretId=arn)["SecretString"])
SQL_PASSWORD = secret["SqlPassword"]
// Node β AWS SDK v3
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"
const client = new SecretsManagerClient({})
const resp = await client.send(new GetSecretValueCommand({ SecretId: process.env.SECRET_ARN }))
const secret = JSON.parse(resp.SecretString)
const SQL_PASSWORD = secret.SqlPassword
Mount as env vars or files; prefer external secret operators to map from Key Vault/Secrets Manager.
apiVersion: v1
kind: Secret
metadata: { name: api-secrets }
type: Opaque
stringData:
SQLSERVER_PASSWORD: "use-external-store-in-prod"
Pattern: fetch once at startup β cache in memory β never log values.
π¦ Containers & runtime configβ
- Docker run:
--env-file .envfor 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.
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_orVITE_is public).
β IG & safety checklistβ
.envin.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β
Whatβs next?
Youβve completed the Learn β Secrets stage. Keep momentum: