Secrets¶
Overview¶
Secrets are stored in AWS Systems Manager Parameter Store as SecureString parameters, encrypted with KMS. ECS
tasks read secrets at startup via the secrets block in their container definitions.
Terraform creates the parameters with placeholder values. Real values are set manually after the initial apply.
Naming Convention¶
SSM parameters follow the pattern:
Examples:
/atrax/stage/atrax-api/database_url
/vault/prod/vault-api/clickhouse_password
/core/stage/core-api/auth_secret
Current Parameters¶
Core API¶
| Path | Description | Status |
|---|---|---|
/core/stage/core-api/auth_secret |
JWT/HMAC signing secret | Set |
/core/stage/core-api/database_url |
MariaDB connection URL | Not yet created |
Atrax¶
The same paths exist in both stage and prod (substitute {env} for stage or prod).
| Path | Description |
|---|---|
/atrax/{env}/atrax-api/database_url |
PostgreSQL connection URL — must include ?uselibpqcompat=true&sslmode=require for Prisma's pg adapter, and the password must be URL-encoded (see Atrax service docs) |
/atrax/{env}/atrax-api/controller_auth |
Bearer tokens accepted by atrax-api (comma-separated) |
/atrax/{env}/atrax-api/s3_access_key_id |
S3 access key — populated from the terraform-managed {env}-euc1-atrax-api-s3 IAM user |
/atrax/{env}/atrax-api/s3_secret_access_key |
S3 secret key — same IAM user |
/atrax/{env}/atrax-node/controller_url |
Atrax API URL (plain String): https://atrax-api.{env}.cookiehub.net for stage, https://atrax-api.cookiehub.net for prod |
/atrax/{env}/atrax-node/controller_auth |
Bearer token used by atrax-node — must match one of atrax-api's tokens |
/atrax/{env}/atrax-node/bugsnag_api_key |
Error tracking key |
The two S3 secrets are populated from terraform outputs (the IAM user and access key are TF-managed):
# from environments/{env}/eu-central-1
aws ssm put-parameter --type SecureString --overwrite \
--name /atrax/{env}/atrax-api/s3_access_key_id \
--value "$(terraform output -raw atrax_api_s3_access_key_id)" \
--region eu-central-1
aws ssm put-parameter --type SecureString --overwrite \
--name /atrax/{env}/atrax-api/s3_secret_access_key \
--value "$(terraform output -raw atrax_api_s3_secret_access_key)" \
--region eu-central-1
Vault¶
| Path | Description |
|---|---|
/vault/{env}/postgres/password |
PostgreSQL master password |
/vault/{env}/postgres/db_url |
PostgreSQL connection URL |
/vault/{env}/mariadb/password |
MariaDB master password (stage only) |
/vault/{env}/mariadb/db_url |
MariaDB connection URL (stage only) |
/{group}/{env}/vault-api/clickhouse_url |
ClickHouse HTTP URL |
/{group}/{env}/vault-api/clickhouse_username |
ClickHouse username |
/{group}/{env}/vault-api/clickhouse_password |
ClickHouse password |
/{group}/{env}/vault-api/vault_api_key |
Static API key for auth |
/{group}/{env}/etl/vault_ingest_config |
JSON config for ETL pipeline |
/{group}/{env}/clickhouse/admin_password |
ClickHouse admin password |
How Secrets Work in Terraform¶
Secrets are defined with a placeholder value and ignore_changes on the value:
resource "aws_ssm_parameter" "database_url" {
name = "/atrax/${var.environment}/atrax-api/database_url"
description = "PostgreSQL connection URL for atrax-api"
type = "SecureString"
value = "placeholder"
lifecycle {
ignore_changes = [value]
}
tags = merge(var.base_tags, { Service = "atrax-api" })
}
This means:
terraform applycreates the parameter once with"placeholder"- After apply, you set the real value manually via CLI
- Subsequent
terraform applyruns leave the value untouched
How ECS Tasks Consume Secrets¶
In the task definition, plain config goes in environment and secrets go in secrets:
container_definitions = jsonencode([{
name = "atrax-api"
image = "${ecr_repo_url}:latest"
environment = [
{ name = "PORT", value = "3000" },
]
secrets = [
{ name = "DATABASE_URL", valueFrom = aws_ssm_parameter.database_url.arn },
{ name = "AUTH_SECRET", valueFrom = aws_ssm_parameter.auth_secret.arn },
]
}])
The ECS agent fetches secrets from SSM at task startup and injects them as environment variables. This requires the
execution role to have ssm:GetParameter and kms:Decrypt permissions:
resource "aws_iam_policy" "read_ssm" {
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ssm:GetParameter", "ssm:GetParameters"]
Resource = [aws_ssm_parameter.database_url.arn]
},
{
Effect = "Allow"
Action = ["kms:Decrypt", "kms:DescribeKey"]
Resource = "*"
}
]
})
}
KMS Encryption¶
- SSM parameters use the AWS-managed
aws/ssmKMS key automatically - RDS instances use dedicated customer-managed KMS keys with:
- Automatic annual key rotation (
enable_key_rotation = true) - 30-day deletion window
prevent_destroylifecycle protection
Setting and Rotating Secrets¶
Set a new secret¶
aws ssm put-parameter \
--name "/core/stage/core-api/database_url" \
--value "mysql://user:pass@host:3306/dbname" \
--type SecureString \
--region eu-central-1
Update an existing secret¶
aws ssm put-parameter \
--name "/core/stage/core-api/database_url" \
--value "mysql://user:newpass@host:3306/dbname" \
--type SecureString \
--overwrite \
--region eu-central-1
Restart service to pick up new value¶
ECS tasks only read secrets at startup. After changing a parameter, force a new deployment:
aws ecs update-service \
--cluster stage-euc1-core-ecs-cluster \
--service core-api \
--force-new-deployment \
--region eu-central-1
Read a secret (for debugging)¶
aws ssm get-parameter \
--name "/core/stage/core-api/auth_secret" \
--with-decryption \
--region eu-central-1 \
--query "Parameter.Value" \
--output text
Database passwords
RDS passwords have ignore_changes = [password] in Terraform. If you rotate a database
password, you must update both the RDS instance password and the corresponding SSM parameter,
then restart dependent services.