Gestion de Secretos — Plataforma Imagy
Principios Fundamentales
- AWS Secrets Manager es la unica fuente de verdad en produccion
- Los secretos nunca se hardcodean en codigo, imagenes Docker, ni variables de CI
- Cada servicio solo accede a sus propios secretos (least privilege)
- Los secretos se rotan periodicamente segun su tipo
- KMS gestiona todas las claves de encriptacion
Fuentes de Secretos por Ambiente
| Ambiente | Fuente | Acceso |
|---|---|---|
| Local (dev) | Archivos .env | Filesystem local |
| CI/CD | GitHub Actions Secrets + AWS Secrets Manager | OIDC federation |
| Staging | AWS Secrets Manager | IAM Role (ECS Task Role) |
| Production | AWS Secrets Manager | IAM Role (ECS Task Role) |
Convencion de Nombres
Los secretos en AWS Secrets Manager siguen esta estructura jerarquica:
imagy/{environment}/{service}/{secret-name}Ejemplos
imagy/production/lending/database
imagy/production/lending/rabbitmq
imagy/production/lending/keycloak-client
imagy/production/identity/keycloak-admin
imagy/production/platform/jwt-signing-key
imagy/staging/lending/database
imagy/staging/flows/rabbitmqEstructura del Valor (JSON)
Cada secreto almacena un JSON con campos especificos segun el tipo:
// imagy/production/lending/database
{
"host": "imagy-lending-prod.cluster-xxxxx.us-east-1.rds.amazonaws.com",
"port": 5432,
"database": "imagy_lending",
"username": "lending_app",
"password": "generated-secure-password",
"connection_string": "Host=...;Port=5432;Database=imagy_lending;Username=lending_app;Password=..."
}
// imagy/production/lending/rabbitmq
{
"host": "imagy-prod-mq.xxxxx.mq.us-east-1.amazonaws.com",
"port": 5671,
"username": "lending_service",
"password": "generated-secure-password",
"virtual_host": "imagy-prod",
"connection_string": "amqps://lending_service:...@host:5671/imagy-prod"
}
// imagy/production/lending/keycloak-client
{
"realm": "imagy",
"client_id": "imagy-lending-api",
"client_secret": "generated-client-secret",
"authority": "https://auth.reimaginetech.io/realms/imagy"
}Acceso a Secretos desde Servicios
Patron EnvConfig
Los servicios acceden a secretos al inicio usando el patron EnvConfig. Este patron resuelve secretos desde AWS Secrets Manager durante el bootstrap de la aplicacion y los inyecta como configuracion tipada.
// Program.cs — configuracion al inicio
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsProduction() || builder.Environment.IsStaging())
{
// Cargar secretos desde AWS Secrets Manager
builder.Configuration.AddSecretsManager(configurator: options =>
{
var environment = builder.Environment.EnvironmentName.ToLower();
var service = "lending";
options.SecretFilter = entry =>
entry.Name.StartsWith($"imagy/{environment}/{service}/");
options.KeyGenerator = (_, secretName) =>
secretName.Replace($"imagy/{environment}/{service}/", "")
.Replace("/", ":");
});
}
else
{
// Local: cargar desde .env
DotNetEnv.Env.Load();
}
// Registrar configuracion tipada
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("database"));
builder.Services.Configure<RabbitMqOptions>(
builder.Configuration.GetSection("rabbitmq"));
builder.Services.Configure<KeycloakOptions>(
builder.Configuration.GetSection("keycloak-client"));Opciones Tipadas
public class DatabaseOptions
{
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 5432;
public string Database { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string ConnectionString { get; set; } = string.Empty;
}
public class RabbitMqOptions
{
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 5671;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string VirtualHost { get; set; } = string.Empty;
public string ConnectionString { get; set; } = string.Empty;
}IAM Policy para ECS Task Role
Cada servicio tiene un Task Role con acceso restringido unicamente a sus secretos:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:imagy/production/lending/*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"arn:aws:kms:us-east-1:123456789012:key/imagy-secrets-key-id"
]
}
]
}Desarrollo Local — Archivos .env
En desarrollo local, los secretos se almacenan en archivos .env que estan en .gitignore.
Estructura
src/
├── Imagy.Lending.Api/
│ ├── .env ← secretos locales (en .gitignore)
│ ├── .env.example ← plantilla SIN valores reales (en git)
│ └── appsettings.json.env.example (committed al repo)
# Database
DATABASE__HOST=localhost
DATABASE__PORT=5432
DATABASE__DATABASE=imagy_lending
DATABASE__USERNAME=imagy
DATABASE__PASSWORD=<your-local-password>
DATABASE__CONNECTION_STRING=Host=localhost;Port=5432;Database=imagy_lending;Username=imagy;Password=<your-local-password>
# RabbitMQ
RABBITMQ__HOST=localhost
RABBITMQ__PORT=5672
RABBITMQ__USERNAME=guest
RABBITMQ__PASSWORD=<your-local-password>
RABBITMQ__VIRTUAL_HOST=/
RABBITMQ__CONNECTION_STRING=amqp://guest:<password>@localhost:5672/
# Keycloak
KEYCLOAK_CLIENT__REALM=imagy
KEYCLOAK_CLIENT__CLIENT_ID=imagy-lending-api
KEYCLOAK_CLIENT__CLIENT_SECRET=<your-client-secret>
KEYCLOAK_CLIENT__AUTHORITY=http://localhost:8080/realms/imagy
# Valkey
VALKEY__CONNECTION_STRING=localhost:6379.gitignore (obligatorio)
# Secretos locales
.env
.env.local
.env.*.local
*.pfx
*.keyKMS — Claves de Encriptacion
AWS KMS gestiona las claves de encriptacion para datos sensibles en reposo y para firmar tokens.
Claves por Proposito
| Clave | Uso | Rotacion |
|---|---|---|
imagy-secrets-key | Encriptar secretos en Secrets Manager | Automatica (anual) |
imagy-data-key | Encriptar datos sensibles en Aurora (TDE) | Automatica (anual) |
imagy-jwt-signing-key | Firmar tokens JWT internos | Manual (cada 6 meses) |
imagy-document-key | Encriptar documentos en S3 | Automatica (anual) |
Politica de Acceso a KMS
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowServiceDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/imagy-lending-task-role"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*"
},
{
"Sid": "AllowAdminManage",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/imagy-platform-admin"
},
"Action": "kms:*",
"Resource": "*"
}
]
}Estrategia de Rotacion
Cada tipo de secreto tiene una frecuencia de rotacion diferente segun su riesgo y complejidad de cambio.
| Tipo de Secreto | Frecuencia | Metodo | Downtime |
|---|---|---|---|
| Database passwords | 90 dias | Rotacion automatica (Secrets Manager) | Zero — dual-user rotation |
| RabbitMQ credentials | 90 dias | Rotacion automatica | Zero — recrear conexiones |
| Keycloak client secrets | 180 dias | Manual con script | Zero — grace period |
| JWT signing keys | 180 dias | Manual — JWKS rotation | Zero — ambas claves validas |
| API keys de terceros | Segun proveedor | Manual | Depende del proveedor |
| KMS keys | 365 dias | Automatica (AWS managed) | Zero |
Rotacion Automatica de Database (Dual-User)
AWS Secrets Manager rota credenciales de base de datos usando la estrategia dual-user: mantiene dos usuarios activos y alterna entre ellos.
Configuracion de Rotacion Automatica (Terraform)
resource "aws_secretsmanager_secret_rotation" "lending_db" {
secret_id = aws_secretsmanager_secret.lending_db.id
rotation_lambda_arn = aws_lambda_function.secret_rotation.arn
rotation_rules {
automatically_after_days = 90
}
}Que Hacer Cuando un Secreto se Compromete
Si se detecta o sospecha que un secreto fue expuesto, seguir este protocolo de respuesta inmediata:
Protocolo de Respuesta
Pasos Detallados
- Rotar inmediatamente — No esperar. Generar nuevo valor y actualizar en Secrets Manager.
# Rotar secreto de database
aws secretsmanager rotate-secret \
--secret-id imagy/production/lending/database \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:rotate-db-secret- Reiniciar servicios afectados — Forzar que los servicios tomen el nuevo secreto.
# Forzar nuevo deployment (toma nuevos secretos al iniciar)
aws ecs update-service \
--cluster imagy-production \
--service imagy-lending \
--force-new-deployment- Revisar CloudTrail — Buscar accesos no autorizados usando el secreto comprometido.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceName,AttributeValue=imagy/production/lending/database \
--start-time "2025-01-01T00:00:00Z"Evaluar impacto — Determinar si hubo acceso no autorizado a datos.
Notificar — Informar al equipo de seguridad y, si aplica, al DPO para evaluacion de breach.
Post-mortem — Documentar como se expuso el secreto y que controles agregar para prevenirlo.
Causas Comunes de Exposicion
| Causa | Prevencion |
|---|---|
| Commit accidental al repo | Pre-commit hooks con gitleaks |
| Log con secreto en texto plano | Sanitizar logs, no loggear connection strings |
| Variable de entorno en error dump | Filtrar env vars en error reporting |
| Secreto en imagen Docker | Multi-stage builds, no copiar .env |
| Compartido por Slack/email | Usar Secrets Manager links, nunca texto plano |
Herramientas de Prevencion
Pre-commit Hook (gitleaks)
Todos los repositorios deben tener gitleaks configurado como pre-commit hook para detectar secretos antes de que lleguen al repositorio.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaksReglas de Deteccion Personalizadas
# .gitleaks.toml
[extend]
useDefault = true
[[rules]]
id = "imagy-connection-string"
description = "Imagy database connection string"
regex = '''Host=.+;.*Password=.+'''
tags = ["connection-string"]
[[rules]]
id = "imagy-secret-path"
description = "AWS Secrets Manager path with value"
regex = '''imagy/(production|staging)/\w+/\w+\s*[:=]\s*.+'''
tags = ["aws-secret"]
[allowlist]
paths = [
'''.env.example''',
'''docs/''',
]Checklist para Nuevos Servicios
Al crear un nuevo servicio, verificar que se cumplan todos estos puntos:
- [ ] Secretos creados en AWS Secrets Manager con la convencion de nombres
- [ ] IAM Task Role con acceso solo a los secretos del servicio
- [ ] KMS key policy actualizada si se necesita una nueva clave
- [ ]
.env.examplecreado con todos los secretos necesarios (sin valores reales) - [ ]
.enven.gitignore - [ ]
gitleaksconfigurado como pre-commit hook - [ ] Rotacion automatica configurada para database credentials
- [ ] Logs sanitizados — no se loggean connection strings ni tokens
- [ ] Documentacion actualizada con los secretos que usa el servicio