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
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
.envsolo 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
.envlocal - 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.enval arrancar - Sin rotación automática (rotación manual cuando sea necesario)