Skip to content

Gestion de Secretos — Plataforma Imagy

Principios Fundamentales

  1. AWS Secrets Manager es la unica fuente de verdad en produccion
  2. Los secretos nunca se hardcodean en codigo, imagenes Docker, ni variables de CI
  3. Cada servicio solo accede a sus propios secretos (least privilege)
  4. Los secretos se rotan periodicamente segun su tipo
  5. KMS gestiona todas las claves de encriptacion

Fuentes de Secretos por Ambiente

AmbienteFuenteAcceso
Local (dev)Archivos .envFilesystem local
CI/CDGitHub Actions Secrets + AWS Secrets ManagerOIDC federation
StagingAWS Secrets ManagerIAM Role (ECS Task Role)
ProductionAWS Secrets ManagerIAM 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/rabbitmq

Estructura del Valor (JSON)

Cada secreto almacena un JSON con campos especificos segun el tipo:

json
// 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.

csharp
// 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

csharp
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:

json
{
  "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)

env
# 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)

gitignore
# Secretos locales
.env
.env.local
.env.*.local
*.pfx
*.key

KMS — Claves de Encriptacion

AWS KMS gestiona las claves de encriptacion para datos sensibles en reposo y para firmar tokens.

Claves por Proposito

ClaveUsoRotacion
imagy-secrets-keyEncriptar secretos en Secrets ManagerAutomatica (anual)
imagy-data-keyEncriptar datos sensibles en Aurora (TDE)Automatica (anual)
imagy-jwt-signing-keyFirmar tokens JWT internosManual (cada 6 meses)
imagy-document-keyEncriptar documentos en S3Automatica (anual)

Politica de Acceso a KMS

json
{
  "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 SecretoFrecuenciaMetodoDowntime
Database passwords90 diasRotacion automatica (Secrets Manager)Zero — dual-user rotation
RabbitMQ credentials90 diasRotacion automaticaZero — recrear conexiones
Keycloak client secrets180 diasManual con scriptZero — grace period
JWT signing keys180 diasManual — JWKS rotationZero — ambas claves validas
API keys de tercerosSegun proveedorManualDepende del proveedor
KMS keys365 diasAutomatica (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)

hcl
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

  1. Rotar inmediatamente — No esperar. Generar nuevo valor y actualizar en Secrets Manager.
bash
# 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
  1. Reiniciar servicios afectados — Forzar que los servicios tomen el nuevo secreto.
bash
# Forzar nuevo deployment (toma nuevos secretos al iniciar)
aws ecs update-service \
  --cluster imagy-production \
  --service imagy-lending \
  --force-new-deployment
  1. Revisar CloudTrail — Buscar accesos no autorizados usando el secreto comprometido.
bash
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=imagy/production/lending/database \
  --start-time "2025-01-01T00:00:00Z"
  1. Evaluar impacto — Determinar si hubo acceso no autorizado a datos.

  2. Notificar — Informar al equipo de seguridad y, si aplica, al DPO para evaluacion de breach.

  3. Post-mortem — Documentar como se expuso el secreto y que controles agregar para prevenirlo.

Causas Comunes de Exposicion

CausaPrevencion
Commit accidental al repoPre-commit hooks con gitleaks
Log con secreto en texto planoSanitizar logs, no loggear connection strings
Variable de entorno en error dumpFiltrar env vars en error reporting
Secreto en imagen DockerMulti-stage builds, no copiar .env
Compartido por Slack/emailUsar 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.

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

Reglas de Deteccion Personalizadas

toml
# .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.example creado con todos los secretos necesarios (sin valores reales)
  • [ ] .env en .gitignore
  • [ ] gitleaks configurado 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

Reimagine Tech LLC — Documentacion Interna