engineering-handbook/secrets-and-config.md
Adam Moussa 9c65fcb053
Some checks failed
ci / ci / ci (push) Has been cancelled
ci: add markdown-lint and link-check CI (INFRA-128) (#16)
Add a standalone ci workflow so handbook changes get an automated gate.
The job is named literally "ci / ci" to emit the exact status context the
org main-branch-protection ruleset requires.

- markdownlint-cli2 (.markdownlint-cli2.jsonc): MD013/MD060/MD040 relaxed
  as noisy docs-style rules; fixed 3 MD032 blank-line-around-list issues.
- lychee link check (lychee.toml): internal + external links, tolerates 429.
2026-07-08 16:20:28 -04:00

69 lines
1.9 KiB
Markdown

# Secrets and Configuration
## The Boundary
There is a strict separation between sensitive and non-sensitive configuration. No gray areas.
## AWS Secrets Manager
Use Secrets Manager for **all** sensitive values:
- API access tokens and keys
- Signing values used for request verification
- Webhook URLs that act as implicit authentication
- Database connection strings with embedded passwords
- Any value that would be dangerous if leaked
When in doubt about whether something qualifies as sensitive, treat it as sensitive.
### Naming Convention
```
stack-name/value-name
```
Examples:
- `my-stack/slack-signing`
- `my-stack/stripe-key`
## SSM Parameter Store
Use Parameter Store **only** for non-sensitive configuration:
- Feature flags
- Endpoint URLs (public, non-authenticated)
- Schedule expressions
- Channel IDs and non-sensitive identifiers
## Lambda Pattern
1. Store the sensitive value in Secrets Manager
2. Grant the function's IAM role `secretsmanager:GetSecretValue` scoped to only the values it needs
3. Read the value on cold start via the AWS SDK
4. Cache it in a module-level variable so subsequent invocations reuse it
```python
import boto3
import json
_client = boto3.client('secretsmanager')
_cached = None
def get_config():
global _cached
if _cached is None:
resp = _client.get_secret_value(SecretId='my-stack/config')
_cached = json.loads(resp['SecretString'])
return _cached
def handler(event, context):
config = get_config()
# use config values
```
## What NOT to Do
- **Never use Lambda environment variables for sensitive values.** Even with `NoEcho` CloudFormation parameters, the values end up as plaintext in the Lambda console and are readable by anyone with `GetFunctionConfiguration` access.
- **Never commit `.env` files** containing real values to a repository.
- **Never store sensitive values** in Notion, Slack messages, or other plaintext documents.