Configuration, Secrets, and Twelve-Factor Boundaries
LESSON
Configuration, Secrets, and Twelve-Factor Boundaries
By the end of this lesson, you will be able to...
Separate code, deploy-time configuration, and secrets in a backend service.
Trace startup configuration loading from raw strings to a safe readiness decision.
Recognize configuration drift, unsafe secret logging, and rotation risks before a service handles traffic.
Idea in one sentence: Configuration is deploy-time input for one environment; secrets are sensitive configuration that need stricter storage, logging, and rotation boundaries.
Core Insight
The previous lesson asked: which test boundary catches this failure?
Now the orders API passes its tests. The artifact deploys. Production starts.
Then checkout begins failing.
The worker logs look normal at first:
worker started
queue connected
payment client initialized
Payment authorizations fail for real customers. After an hour, the team finds the cause:
APP_ENV=production
PAYMENT_API_BASE_URL=https://staging-payments.example
PAYMENT_API_KEY=staging key
The code was correct. The tests were correct for the code they exercised. The deploy-time configuration was wrong.
This is a different backend failure from a bad SQL query, a missing authorization check, or a provider contract mismatch. The same code changed behavior because values outside the source code pointed it at the wrong environment.
The naive idea is:
Configuration is just a bag of strings the app reads when it needs them.
That is too loose.
A better model is:
Configuration is a startup boundary.
The service receives deploy-time instructions.
It parses them.
It validates them.
It refuses unsafe combinations.
It reports safe evidence.
Only then should it become ready.
Secrets fit inside that model, but they need tighter handling. A payment key, database password, or session signing secret is not just another string. Possession may grant access.
The Small Situation
Use one service: orders-worker.
It processes checkout jobs and calls a payment provider.
It needs values like:
APP_ENV=production
DATABASE_URL=postgres://...
PAYMENT_API_BASE_URL=https://api.payments.example
PAYMENT_API_KEY=...
SESSION_COOKIE_SECRET=...
ORDER_QUEUE_NAME=orders-production
LOG_LEVEL=info
FEATURE_NEW_CHECKOUT=false
REQUEST_TIMEOUT_MS=3000
These values are not all the same kind of thing.
- Code is the source and built artifact: handlers, policies, SQL, tests, and runtime package.
- Configuration is deploy-time input that changes behavior without changing code.
- Secrets are sensitive configuration, such as API keys, private keys, database passwords, and signing secrets.
- Environment is where the service runs: local, test, staging, production, or preview.
- Startup validation is the service refusing missing, malformed, or unsafe configuration before it handles work.
- Rotation is replacing a secret safely while old clients, sessions, or workers may still exist.
Plain meaning:
Configuration tells the same release how to run in one place.
In this scenario:
PAYMENT_API_BASE_URL tells the worker which payment provider endpoint to call in production.
Technical name:
This is deploy-time configuration. The Twelve-Factor App guideline says configuration that varies between deploys should be separated from code. Environment variables are one common interface, but the deeper idea is the boundary: code and deploy-specific values should not be tangled together.
Plain meaning:
A secret is a configuration value that must not be casually exposed.
In this scenario:
PAYMENT_API_KEY and SESSION_COOKIE_SECRET are secrets because they can authorize sensitive actions or protect authentication evidence.
Technical name:
These values need secret management: controlled storage, injection, access limits, redaction, and rotation.
A Startup Configuration Trace
Trace the worker from process launch to readiness.
Input:
raw environment variables
secret references from deployment platform
Step 1: process starts
no checkout jobs should run yet
Step 2: read raw values
APP_ENV="production"
REQUEST_TIMEOUT_MS="3000"
FEATURE_NEW_CHECKOUT="false"
PAYMENT_API_KEY is present from secret manager
Intermediate state:
values are still strings or secret handles
Step 3: parse typed settings
app_env = production
request_timeout_ms = 3000
new_checkout_enabled = false
payment_api_key = present secret
Step 4: validate required values
DATABASE_URL exists
PAYMENT_API_BASE_URL exists
PAYMENT_API_KEY exists
SESSION_COOKIE_SECRET exists
Step 5: validate meaning
timeout is within allowed range
log level is allowed
production does not point at staging payment URL
production queue name matches production environment
Step 6: initialize clients
database client created
payment client created
queue client created
Step 7: emit safe startup evidence
app_env=production
payment_base=api.payments.example
payment_key=present version=payments-prod-v4
order_queue=orders-production
timeout_ms=3000
Output:
service becomes ready for work
Now compare the broken path:
Input:
APP_ENV=production
PAYMENT_API_BASE_URL=https://staging-payments.example
PAYMENT_API_KEY=staging key
Naive startup:
every variable exists
worker becomes ready
checkout jobs call staging provider
production payments fail
The missing check was not "is the value present?"
The missing check was:
Are these values coherent for this environment?
That is why startup validation should check presence, type, range, and unsafe combinations.
Check: APP_ENV=production and PAYMENT_API_BASE_URL=https://staging-payments.example are both present and parseable. Should startup validation pass?
Think first, then reveal.
Answer: No. The values exist, but they are incoherent. A production worker should not silently call a staging payment provider unless there is an explicit, controlled exception.
Secrets Need Different Handling
All secrets are configuration.
Not all configuration is secret.
LOG_LEVEL=info is configuration, but it is usually not secret. PAYMENT_API_KEY is configuration and secret. Treating both the same creates risk.
Secrets need stricter handling because possession often grants power:
- a database password may allow data access,
- a payment API key may move money or reveal transaction data,
- a session signing secret may allow forged sessions,
- a private key may impersonate a service.
Good secret handling includes:
- store secrets outside source control,
- inject secrets through deployment or a secret manager,
- avoid printing secrets in logs, errors, metrics labels, test output, or screenshots,
- keep separate secrets for local, staging, and production,
- limit each secret to the permissions it needs,
- rotate secrets with a planned transition.
Safe logs should describe secret state without revealing secret material.
Prefer:
payment_api_key=present version=payments-prod-v4
session_cookie_secret=present version=session-2026-07
Avoid:
PAYMENT_API_KEY=sk_live_full_secret_value_here
SESSION_COOKIE_SECRET=full_secret_here
Even partial secrets can be risky. Printing the first eight characters may feel harmless, but providers sometimes encode meaning in prefixes, and partial leaks can combine with other evidence. Use secret version IDs, names, or metadata instead.
Check: Should startup logs print the first eight characters of a production API key to help debugging?
Think first, then reveal.
Answer: Usually no. Prefer safe presence checks and secret version metadata. Logs should help operators debug without leaking key material.
Rotation Is a Workflow
Changing a secret is not always one edit.
Suppose SESSION_COOKIE_SECRET signs user sessions.
If you replace it instantly, every existing session may fail verification. That may be acceptable during an incident. It is usually too disruptive for routine rotation.
A rotation-friendly design might use one key for signing and multiple keys for verification:
before:
sign with: key_2026_06
verify with: key_2026_06
during rotation:
sign with: key_2026_07
verify with: key_2026_07, key_2026_06
after old sessions expire:
sign with: key_2026_07
verify with: key_2026_07
Payment keys may need a different workflow:
1. create new provider key with the right permissions
2. deploy support for reading the new secret version
3. switch traffic to the new key
4. verify payment success and error rates
5. revoke the old key
The constant idea is:
Rotation changes live behavior.
Plan overlap.
Verify success.
Remove old access.
Rotation is also a testing and deployment concern. If the app only supports one signing key, routine rotation becomes a user-facing logout event. If workers cache secrets forever, rotation may not take effect until every worker restarts.
Drift, Flags, and Safe Evidence
Configuration drift means environments differ in ways the team cannot quickly explain.
Some differences are intended:
local uses local database
staging uses staging payment provider
production uses live payment provider
The dangerous differences are untracked or unvalidated:
production has an old timeout
staging uses a different feature flag
one worker has a stale secret version
debug logging is accidentally enabled in production
Feature flags are configuration too. A flag such as FEATURE_NEW_CHECKOUT=true can help gradual rollout. It can also make staging and production exercise different code paths. For critical paths, know who owns the flag, when it expires, and what evidence shows which value was active.
Good operational evidence is safe and specific:
app_env=production
release_sha=abc123
config_version=orders-prod-2026-07-01
new_checkout_enabled=false
payment_provider=live
payment_api_key_version=payments-prod-v4
This evidence does not replace secret protection. It helps operators answer:
- Which release is running?
- Which environment is this?
- Which config version was loaded?
- Which safe feature flag values were active?
- Which secret version, not secret value, was used?
Do not log every configuration value on every request. That creates noise and risk. Prefer startup summaries, deployment records, readiness state, and safe metrics dimensions with low cardinality.
A Small Decision Table
When reviewing a new setting, ask what kind of value it is before deciding where it belongs.
Value Kind Handling
APP_ENV config validate allowed value
REQUEST_TIMEOUT_MS config parse integer and range
FEATURE_NEW_CHECKOUT config record owner and expiry
PAYMENT_API_BASE_URL config validate environment match
PAYMENT_API_KEY secret config inject securely, redact logs
SESSION_COOKIE_SECRET secret config support rotation overlap
DATABASE_URL sensitive config restrict access, avoid logs
This table helps because "configuration" is not one storage bucket. A timeout, a feature flag, and a signing secret all arrive through the deploy boundary, but they do not deserve the same treatment.
A timeout should be easy to see in safe startup evidence because it affects behavior and helps debugging. A feature flag should have ownership because it changes which code path runs. A payment key should be present and versioned, but not visible. A session signing secret should have a rotation plan because old sessions may still need verification.
The design review question is:
If this value is wrong, when do we want to find out?
For required values, the answer is usually startup. Missing DATABASE_URL or malformed REQUEST_TIMEOUT_MS should stop readiness before the service handles work.
For dangerous combinations, the answer is also startup. Production pointed at staging payments is not a runtime surprise we should let customers discover.
For values that intentionally vary, the answer is safe evidence. Operators should be able to see that FEATURE_NEW_CHECKOUT=false was active for release abc123 without exposing secrets or dumping every environment variable.
This connects back to testing. A unit test can prove the parser rejects "ten_seconds". An integration or deployment smoke test can prove the service refuses readiness with incoherent production settings. But the operational habit is bigger than one test: make configuration reviewable as a boundary.
Trade-offs and Limits
Separating code from configuration makes deployment flexible. The same artifact can run locally, in staging, and in production.
It also creates a new failure mode: the artifact may be correct while the environment instructions are wrong.
The main trade-off is flexible deployment versus configuration drift.
Moving values out of code means one release can run in many environments. That is useful. It also means two environments can quietly diverge until the wrong value causes a production failure.
Startup validation improves safety because the service fails before handling traffic. It costs discipline. Teams must maintain a schema for settings, reject unsafe combinations, and keep environment rules current.
Secret management reduces leak risk. It costs operational work: access control, rotation planning, audit trails, and platform integration.
Safe config evidence improves debugging. It costs judgment because too much detail leaks information or creates noisy logs.
This lesson does not prove dependencies are healthy. A valid DATABASE_URL can still point to a down database. Configuration validation answers:
Did the service receive coherent instructions?
Readiness and health checks answer:
Can the service use what it needs right now?
The signal that this boundary is weak is an incident where the code and tests are fine, but a deploy-time value makes the service behave like the wrong environment.
Practice
Review this deployment configuration:
APP_ENV=production
DATABASE_URL=postgres://orders-prod
PAYMENT_API_BASE_URL=https://staging-payments.example
PAYMENT_API_KEY=present
SESSION_COOKIE_SECRET=present
LOG_LEVEL=debug
REQUEST_TIMEOUT_MS=ten_seconds
FEATURE_NEW_CHECKOUT=true
A good answer should mention:
- All values are deploy-time configuration.
DATABASE_URL,PAYMENT_API_KEY, andSESSION_COOKIE_SECRETare sensitive or secret-bearing values.- Startup validation should reject production using a staging payment base URL.
REQUEST_TIMEOUT_MS=ten_secondsshould fail type parsing because the app expects an integer.LOG_LEVEL=debugin production should be rejected or require an explicit controlled exception.- The service should not become ready.
- Logs may safely show environment, config version, provider family, timeout parse failure, and secret version metadata, but not raw secret values.
- A rotation concern is that changing
SESSION_COOKIE_SECRETmay invalidate existing sessions unless old verification keys remain during a transition.
Model answer:
This service should not become ready. The values are present, but the production environment points at the staging payment provider, the timeout is not parseable as an integer, and debug logging in production is risky unless explicitly approved. The startup summary can say payment_key=present and name a secret version, but it should not print the key. For session secret rotation, the service should support signing with the new key while verifying old sessions until they expire.
Connections
Lesson 011 asked what a test boundary can prove. This lesson shows a failure that tests may miss if the wrong values are injected at deploy time.
The next lesson uses the same boundary thinking for containers: what belongs in the deployable image, and what must stay outside as runtime configuration or external dependency state.
Resources
- [REFERENCE] The Twelve-Factor App: Config
- Link: https://12factor.net/config
- Focus: Use this for the separation between code and deploy-specific configuration.
- [REFERENCE] OWASP Secrets Management Cheat Sheet
- Link: https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- Focus: Read for secret storage, access control, rotation, and leakage risks.
- [DOC] Kubernetes: Secrets
- Link: https://kubernetes.io/docs/concepts/configuration/secret/
- Focus: Use this as a concrete platform example of injecting sensitive configuration.
- [DOC] Doppler: Secrets Rotation
- Link: https://docs.doppler.com/docs/secrets-rotation
- Focus: Compare one product-oriented explanation of rotation workflows and operational concerns.
Key Takeaways
- Configuration is deploy-time input; secrets are sensitive configuration with stricter handling rules.
- Startup validation should parse values, check required settings, and reject unsafe environment combinations before readiness.
- Safe operational evidence should reveal environment, config version, and secret version metadata without exposing secret material.
- Secret rotation is a workflow with overlap, verification, and old-access removal.
- Separating code from config enables flexible deployment, but it also creates drift unless values are validated and observable.
← Back to Backend Development Foundations