Saltar al contenido principal

AWS Secrets Manager

AWS Secrets Manager es el servicio centralizado de gestión de secretos para todo el ecosistema SwapBits. Almacena credenciales, API keys, claves de encriptación y otros datos sensibles de forma segura.


Arquitectura de Secrets

Arquitectura de Secrets Manager en SwapBits
Click para ampliar (zoom y pan interactivo)

1. Secrets Usados en SwapBits

El sistema utiliza 3 secrets principales en AWS Secrets Manager:

production/env

Contiene todas las variables de entorno de producción para los microservicios:

{
// Database
DB_URI: string, // MongoDB connection string

// Redis
REDIS_URL: string, // Redis connection URL completa

// JWT & Authentication
TOKEN_SECRET: string, // Secret para tokens JWT

// APIs Externas
BYBIT_API_KEY: string, // API key de Bybit
BYBIT_API_SECRET: string, // API secret de Bybit

// Blockchain RPCs
SOLANA_RPC: string, // RPC de Solana
ETHEREUM_RPC: string, // RPC de Ethereum
// ... otros RPCs según necesidad

// Configuraciones adicionales
// ... otros valores según servicio
}

hash/config

Configuración para encriptación y hashing:

{
// Encryption keys para private keys de wallets
MASTER_ENCRYPTION_KEY: string, // Master key para AES-256-GCM
SALT: string, // Salt para key derivation

// Configuraciones de hashing
// ... otros valores de encriptación
}

withdrawal/global/config

Configuración global para retiros y hot wallets:

{
// Hot wallet addresses o keys
// Configuraciones de thresholds de retiro
// Fees y límites
// ... configuración específica de retiros
}

2. Estructura en AWS

Naming Convention Real

# Secrets actuales en el sistema:
production/env # Variables de entorno principales
hash/config # Configuración de encriptación
withdrawal/global/config # Configuración de retiros

Región AWS

Los secrets están alojados en la región eu-north-1 (Estocolmo, Suecia).

Todos los servicios configuran esta región por defecto:

AWS_REGION = process.env.AWS_REGION || 'eu-north-1'

3. Acceso desde Servicios

Patrón de Carga en Microservicios

Todos los microservicios (auth, user-service, wallet-service, bank-service, exchange-service, kyc, ws-service, webhook-service) siguen el mismo patrón:

// main.ts (patrón compartido)
import * as AWS from 'aws-sdk';
import * as dotenv from 'dotenv';

// Cargar .env primero (para development)
dotenv.config({ path: '.env' });

async function loadSecrets(secretId: string) {
const secrets = new AWS.SecretsManager({
region: process.env.AWS_REGION || 'eu-north-1'
});

const res = await secrets.getSecretValue({ SecretId: secretId }).promise();
const parsed = JSON.parse(res.SecretString);

// Inyectar en process.env
for (const [k, v] of Object.entries(parsed)) {
process.env[k] = String(v);
}

console.log(`✓ Secret '${secretId}' cargado`);
}

// Lógica de entorno
(async () => {
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;

if (isDevelopment) {
console.log('🏠 Modo desarrollo - Usando variables del archivo .env');

// Verificar que las variables principales estén cargadas
const requiredVars = ['DB_URI', 'TOKEN_SECRET', 'REDIS_URL'];
const missingVars = requiredVars.filter(v => !process.env[v]);

if (missingVars.length > 0) {
console.error('[ERROR] Variables faltantes en .env:', missingVars);
process.exit(1);
}
}
else if (isProduction) {
console.log('[CLOUD] Modo producción - Cargando secrets desde AWS...');

try {
// Cargar los 3 secrets
await loadSecrets('production/env');
await loadSecrets('hash/config');
await loadSecrets('withdrawal/global/config');

console.log('[SUCCESS] Secrets de AWS cargados correctamente');
} catch (error) {
console.error('[ERROR] Error cargando secrets:', error.message);
throw new Error('No se pudieron cargar los secrets de AWS');
}
}

// Iniciar aplicación NestJS
const app = await NestFactory.create(AppModule);
// ... resto de configuración
await app.listen(3000);
})();

Admin Service (Backend)

El admin service usa el SDK v3 de AWS con configuración avanzada:

// aws.ts
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
import * as https from "https";
import * as dns from "dns";

const AWS_REGION = process.env.AWS_REGION || 'eu-north-1';

// Forzar IPv4 (evita hangs por IPv6)
const ipv4Agent = new https.Agent({
lookup: (hostname, _opts, cb) => dns.lookup(hostname, { family: 4 }, cb),
keepAlive: true,
});

const requestHandlerOpts = {
httpsAgent: ipv4Agent,
connectionTimeout: 3000,
requestTimeout: 5000,
};

function getCredentialsProvider() {
// En Docker/producción, usar variables de entorno si están disponibles
if (process.env.NODE_ENV === 'production' &&
process.env.AWS_ACCESS_KEY_ID &&
process.env.AWS_SECRET_ACCESS_KEY) {
return {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
};
}

// Fallback: cadena completa de credenciales (EC2 role, etc)
return fromNodeProviderChain();
}

export function createSecretsClient() {
return new SecretsManagerClient({
region: AWS_REGION,
credentials: getCredentialsProvider(),
requestHandler: new NodeHttpHandler(requestHandlerOpts),
});
}

export async function loadSecretToEnv(secretId: string) {
const sm = createSecretsClient();
const out = await sm.send(new GetSecretValueCommand({ SecretId: secretId }));

const raw = out.SecretString ??
(out.SecretBinary ? Buffer.from(out.SecretBinary).toString("utf-8") : "{}");

const obj = JSON.parse(raw);

// Inyectar en process.env
for (const [k, v] of Object.entries(obj)) {
process.env[k] = String(v);
}

return Object.keys(obj).length;
}

// Uso en main.ts del admin
async function loadSecrets() {
try {
const n1 = await loadSecretToEnv("production/env");
console.log(`[SECRETS] [OK] production/env → ${n1} vars`);

const n2 = await loadSecretToEnv("withdrawal/global/config");
console.log(`[SECRETS] [OK] withdrawal/global/config → ${n2} vars`);
} catch (error) {
console.error('[SECRETS] [ERROR] Error:', error.message);
throw error;
}
}

Lambda Functions

Las Lambda functions también acceden a secrets (especialmente para blockchain RPCs):

// CreateWalletEvm/index.mjs
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const secretsClient = new SecretsManagerClient({ region: 'eu-north-1' });

// Cache global (persiste entre invocaciones)
let cachedSecrets = null;

export const handler = async (event) => {
// Cargar secrets solo si no están cacheados
if (!cachedSecrets) {
const command = new GetSecretValueCommand({
SecretId: 'production/env',
});

const response = await secretsClient.send(command);
cachedSecrets = JSON.parse(response.SecretString);
console.log('[SUCCESS] Secrets cargados en Lambda');
}

// Usar RPC URL
const rpcUrl = cachedSecrets.ETHEREUM_RPC || 'https://eth.llamarpc.com';

// ... resto de lógica de creación de wallet
};

4. IAM Permissions

Policy para EC2 Instances

Las instancias EC2 necesitan permisos para leer los secrets:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:eu-north-1:ACCOUNT_ID:secret:production/env-*",
"arn:aws:secretsmanager:eu-north-1:ACCOUNT_ID:secret:hash/config-*",
"arn:aws:secretsmanager:eu-north-1:ACCOUNT_ID:secret:withdrawal/global/config-*"
]
},
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:eu-north-1:ACCOUNT_ID:key/*"
}
]
}

Policy para Lambda Functions

Las Lambda functions también necesitan acceso similar:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": [
"arn:aws:secretsmanager:eu-north-1:ACCOUNT_ID:secret:production/env-*",
"arn:aws:secretsmanager:eu-north-1:ACCOUNT_ID:secret:withdrawal/global/config-*"
]
},
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:eu-north-1:ACCOUNT_ID:key/*"
}
]
}

Principio de Least Privilege

Cada servicio solo debe tener acceso a los secrets que necesita:

  • [OK] EC2 instances: production/env, hash/config, withdrawal/global/config
  • [OK] Lambda functions: production/env, withdrawal/global/config
  • [NO] Nunca dar acceso secretsmanager:* a todos los secrets

5. Rotación de Secrets

Proceso Manual de Rotación

Para rotar un secret sin downtime:

# 1. Crear nueva versión del secret
aws secretsmanager put-secret-value \
--secret-id production/env \
--secret-string '{"DB_URI":"new-value", ...}' \
--region eu-north-1

# 2. Los servicios cargan el secret al iniciar, así que:
# - Hacer rolling restart de los servicios
# - Los nuevos containers cargarán el secret actualizado

# 3. Verificar que todo funciona correctamente

# 4. El secret viejo se mantiene versionado (rollback posible)

Estrategia de Rotación Recomendada

# Para cambios críticos (como DB_URI):
# 1. Mantener compatibilidad temporal
# 2. Actualizar secret
# 3. Rolling deployment
# 4. Verificar salud del sistema
# 5. Remover valor viejo después de 24-48h

6. Security Best Practices

Seguridad de Secrets

Nunca hacer:

  • Hardcodear secrets en código
  • Commitear secrets en Git
  • Loggear secrets en consola
  • Exponer secrets en APIs
  • Usar secrets de producción en development
  • Compartir secrets por email/Slack

Siempre hacer:

  • [✓] Usar AWS Secrets Manager en producción
  • [✓] Usar archivo .env solo en development local
  • [✓] Separar secrets por environment (production/staging)
  • [✓] Usar IAM roles restrictivos
  • [✓] Habilitar encryption con KMS
  • [✓] Auditar acceso a secrets vía CloudTrail

Encryption at Rest

Todos los secrets en AWS Secrets Manager están:

  • Encriptados con AWS KMS (key management service)
  • Stored in multiple AZs en eu-north-1 (alta disponibilidad)
  • Versionados (rollback posible)
  • Auditados (CloudTrail logs)

Development vs Production

// ✅ CORRECTO: Usar .env en development, AWS Secrets en production
const isProduction = process.env.NODE_ENV === 'production';

if (isProduction) {
// Cargar desde AWS Secrets Manager
await loadSecrets('production/env');
await loadSecrets('hash/config');
await loadSecrets('withdrawal/global/config');
} else {
// Usar .env local
dotenv.config();
console.log('🏠 Usando variables de .env');
}

7. Monitoreo y Troubleshooting

CloudTrail Logging

Todos los accesos a Secrets Manager se loggean automáticamente:

{
"eventName": "GetSecretValue",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::ACCOUNT:assumed-role/SwapBitsEC2Role/i-..."
},
"requestParameters": {
"secretId": "production/env"
},
"sourceIPAddress": "10.0.1.50",
"eventTime": "2025-10-20T10:30:00Z",
"region": "eu-north-1"
}

Errores Comunes

// Error: ResourceNotFoundException
// Causa: Secret no existe en la región especificada
// Solución: Verificar que el secret existe en eu-north-1

// Error: AccessDeniedException
// Causa: IAM role no tiene permisos
// Solución: Agregar policy secretsmanager:GetSecretValue

// Error: Timeout
// Causa: Problemas de red o IPv6
// Solución: Usar IPv4 agent (como en admin service)

// Error: InvalidRequestException
// Causa: SecretString no es JSON válido
// Solución: Verificar formato JSON del secret

8. Backup y Recovery

Backup Manual

#!/bin/bash
# backup-secrets.sh

SECRETS=(
"production/env"
"hash/config"
"withdrawal/global/config"
)

for SECRET in "${SECRETS[@]}"; do
echo "Backing up $SECRET..."

# Descargar secret
aws secretsmanager get-secret-value \
--secret-id "$SECRET" \
--region eu-north-1 \
--query SecretString \
--output text > "backup-${SECRET//\//-}.json"

echo "[SUCCESS] $SECRET backed up"
done

echo "[WARNING] IMPORTANTE: Encriptar y guardar estos backups de forma segura"
echo "[WARNING] NO commitear a Git"

Recovery

# Restaurar desde backup
aws secretsmanager put-secret-value \
--secret-id production/env \
--secret-string file://backup-production-env.json \
--region eu-north-1

echo "[SUCCESS] Secret restaurado"

Resumen

AWS Secrets Manager en SwapBits:

  • 3 secrets principales: production/env, hash/config, withdrawal/global/config
  • Región: eu-north-1 (Estocolmo, Suecia)
  • Carga automática al iniciar cada microservicio
  • Encriptación con AWS KMS
  • Auditoría automática con CloudTrail
  • Development: usa .env local
  • Production: carga desde AWS Secrets Manager
  • Costo: ~$1.20/mes (3 secrets × $0.40)

Arquitectura:

  • EC2 instances usan IAM role para acceder a secrets (sin credenciales hardcodeadas)
  • Lambda functions también usan IAM role
  • Secrets se inyectan en process.env al arrancar
  • Sin rotación automática (rotación manual cuando sea necesario)