Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
13 KiB
| id | title | type | project | version | updated_date |
|---|---|---|---|---|---|
| ADR-007-security | Seguridad y Autenticación | Documentation | trading-platform | 1.0.0 | 2026-01-04 |
ADR-006: Seguridad y Autenticación
Estado: Aceptado Fecha: 2025-12-06 Decisores: Tech Lead, Arquitecto, Security Engineer Relacionado: ADR-001, ADR-005
Contexto
OrbiQuant IA maneja datos financieros críticos y transacciones reales. Necesitamos:
- Authentication: Identificar usuarios de forma segura
- Authorization: Controlar acceso a recursos (portfolios, predictions, pagos)
- Data Protection: Encriptar datos sensibles (passwords, API keys, PII)
- Attack Prevention: Proteger contra brute force, XSS, CSRF, SQL injection
- Compliance: Preparar para GDPR, PCI-DSS (pagos con Stripe)
- Audit Trail: Logs de acciones críticas (trades, transfers, config changes)
Requisitos de Seguridad:
- Zero-trust: Never trust, always verify
- Defense in depth: Múltiples capas de seguridad
- Fail securely: Errores deben denegar acceso por defecto
Decisión
Authentication Strategy
JWT (JSON Web Tokens) + Refresh Tokens
Access Token (Short-lived)
{
"sub": "user_123abc", // User ID
"email": "user@example.com",
"role": "premium", // User tier
"iat": 1701878400, // Issued at
"exp": 1701882000 // Expires in 1 hour
}
Refresh Token (Long-lived)
- Stored in httpOnly cookie (no JavaScript access)
- TTL: 7 days
- Rotated on each use (automatic refresh)
- Revocable via Redis blacklist
Password Security
bcrypt with cost factor 12
// apps/backend/src/services/auth.service.ts
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12; // 2^12 iterations (~250ms)
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Password Requirements
const PASSWORD_POLICY = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecial: true,
maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days
preventReuse: 5, // Last 5 passwords
};
Multi-Factor Authentication (MFA)
TOTP (Time-based One-Time Password) using speakeasy
// apps/backend/src/services/mfa.service.ts
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
async function generateMFASecret(userId: string) {
const secret = speakeasy.generateSecret({
name: `OrbiQuant IA (${userId})`,
issuer: 'OrbiQuant'
});
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
// Store secret encrypted in DB
await db.user.update({
where: { id: userId },
data: { mfaSecret: encrypt(secret.base32) }
});
return { secret: secret.base32, qrCode };
}
function verifyMFAToken(secret: string, token: string): boolean {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2 // Allow ±2 time steps (60 seconds tolerance)
});
}
Rate Limiting
Redis-based rate limiting (ADR-005)
// apps/backend/src/middleware/rateLimit.ts
const RATE_LIMITS = {
// Auth endpoints (aggressive)
login: { max: 5, window: 60 * 15 }, // 5 attempts per 15 min
register: { max: 3, window: 60 * 60 }, // 3 per hour
resetPassword: { max: 3, window: 60 * 60 },
// API endpoints (generous)
api: { max: 100, window: 60 }, // 100 per minute
apiPremium: { max: 500, window: 60 }, // Premium users
// ML predictions (moderate)
mlPredictions: { max: 20, window: 60 }, // 20 per minute
};
async function rateLimit(
key: string,
config: RateLimitConfig
): Promise<boolean> {
const current = await redis.incr(`ratelimit:${key}`);
if (current === 1) {
await redis.expire(`ratelimit:${key}`, config.window);
}
return current <= config.max;
}
CORS Configuration
Strict CORS for API
// apps/backend/src/config/cors.ts
import cors from 'cors';
const ALLOWED_ORIGINS = process.env.NODE_ENV === 'production'
? ['https://app.orbiquant.com', 'https://orbiquant.com']
: ['http://localhost:5173', 'http://localhost:3000'];
export const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
if (!origin || ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // 24 hours
};
SQL Injection Prevention
Parameterized Queries (Prisma ORM)
// ✅ SAFE - Prisma uses parameterized queries
const user = await db.user.findUnique({
where: { email: userInput }
});
// ❌ NEVER DO THIS
const users = await db.$queryRaw`
SELECT * FROM users WHERE email = ${userInput}
`; // Vulnerable to SQL injection
XSS Prevention
// apps/backend/src/middleware/security.ts
import helmet from 'helmet';
import { sanitize } from 'dompurify';
// Helmet middleware (sets security headers)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid 'unsafe-inline' in prod
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.orbiquant.com"],
},
},
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
}));
// Sanitize user input
function sanitizeInput(input: string): string {
return sanitize(input, {
ALLOWED_TAGS: [], // Strip all HTML
ALLOWED_ATTR: [],
});
}
CSRF Protection
Double Submit Cookie Pattern
// apps/backend/src/middleware/csrf.ts
import { randomBytes } from 'crypto';
function generateCSRFToken(): string {
return randomBytes(32).toString('hex');
}
function csrfProtection(req: Request, res: Response, next: NextFunction) {
// Skip for GET, HEAD, OPTIONS
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const tokenFromHeader = req.headers['x-csrf-token'];
const tokenFromCookie = req.cookies['csrf-token'];
if (!tokenFromHeader || tokenFromHeader !== tokenFromCookie) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
Encryption
AES-256-GCM for sensitive data at rest
// apps/backend/src/utils/crypto.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes
export function encrypt(plaintext: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Return: iv:authTag:ciphertext
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decrypt(ciphertext: string): string {
const [ivHex, authTagHex, encrypted] = ciphertext.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Secure Headers
// Set via Helmet
app.use(helmet({
frameguard: { action: 'deny' }, // X-Frame-Options: DENY
contentSecurityPolicy: true, // CSP
hsts: { maxAge: 31536000 }, // Strict-Transport-Security
noSniff: true, // X-Content-Type-Options: nosniff
xssFilter: true, // X-XSS-Protection
referrerPolicy: { policy: 'strict-origin' }, // Referrer-Policy
}));
Audit Logging
// apps/backend/src/middleware/audit.ts
import { logger } from './logger';
const AUDITABLE_ACTIONS = [
'LOGIN', 'LOGOUT', 'REGISTER',
'PASSWORD_CHANGE', 'MFA_ENABLE', 'MFA_DISABLE',
'TRADE_EXECUTED', 'FUND_TRANSFER', 'API_KEY_CREATED',
'SUBSCRIPTION_CHANGED', 'PAYMENT_PROCESSED',
];
function auditLog(action: string, userId: string, metadata: object) {
if (AUDITABLE_ACTIONS.includes(action)) {
logger.info('audit_log', {
action,
userId,
timestamp: new Date().toISOString(),
ip: metadata.ip,
userAgent: metadata.userAgent,
metadata,
});
}
}
// Example usage
auditLog('TRADE_EXECUTED', user.id, {
symbol: 'AAPL',
quantity: 10,
price: 150.25,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
Consecuencias
Positivas
- Zero Trust: JWT stateless, cada request validado
- MFA Protection: TOTP previene account takeover incluso con password leak
- Rate Limiting: Brute force prácticamente imposible (5 attempts / 15 min)
- Defense in Depth: Múltiples capas (bcrypt, JWT, MFA, rate limit, CORS)
- Audit Trail: Compliance-ready logs de acciones críticas
- Encryption: Datos sensibles encriptados at rest (AES-256-GCM)
- Secure Headers: Helmet previene XSS, clickjacking, MIME sniffing
Negativas
- UX Friction: MFA agrega paso extra en login
- Complexity: Refresh token rotation es complejo de implementar
- Performance: bcrypt (250ms) y MFA verificación agregan latencia
- Token Management: JWT blacklist requiere Redis storage
- Mobile Challenges: httpOnly cookies no funcionan bien en mobile apps
Riesgos y Mitigaciones
| Riesgo | Mitigación |
|---|---|
| JWT secret leak | Rotate secrets monthly, use strong random (32+ bytes) |
| Brute force MFA | Rate limit: 5 attempts → lock account 1 hour |
| Session fixation | Regenerate session ID on login |
| MITM attacks | Enforce HTTPS only, HSTS header |
| Replay attacks | Short JWT TTL (1 hour), nonce for critical actions |
| Account enumeration | Generic error messages ("Invalid credentials") |
Alternativas Consideradas
1. Session-Based Auth (Cookies)
- Pros: Simple, server-side revocation fácil
- Contras: Stateful, no funciona bien con mobile apps
- Decisión: ❌ Descartada - JWT es más flexible para multi-platform
2. OAuth 2.0 + Third-Party (Google, Facebook)
- Pros: No password management, MFA delegado
- Contras: Vendor dependency, no apropiado para finanzas
- Decisión: ⚠️ Complementario - Ofrecer como opción adicional
3. Argon2 para Password Hashing
- Pros: Más seguro que bcrypt, resistente a GPU attacks
- Contras: Menos maduro en Node.js ecosystem
- Decisión: ❌ Descartada - bcrypt es suficientemente seguro
4. SMS-based MFA
- Pros: User-friendly, no app required
- Contras: SIM swapping attacks, costo por SMS
- Decisión: ❌ Descartada - TOTP es más seguro y gratis
5. WebAuthn / FIDO2
- Pros: Phishing-resistant, passwordless future
- Contras: Browser support limitado, complejo de implementar
- Decisión: ❌ Pospuesta - Evaluar post-MVP
6. No Rate Limiting
- Pros: Menos complejidad
- Contras: Vulnerable a brute force y DDoS
- Decisión: ❌ Descartada - Rate limiting es crítico
Security Checklist
Development
- No hardcoded secrets (use .env)
- All inputs sanitized
- SQL injection tests
- XSS tests
- CSRF protection enabled
- Rate limiting configured
- Error messages don't leak info
Deployment
- HTTPS enforced (no HTTP)
- Secrets in environment variables
- Database credentials rotated
- JWT secret rotated monthly
- Helmet middleware enabled
- CORS configured strictly
- Security headers validated
Monitoring
- Failed login attempts tracked
- Rate limit violations alerted
- Suspicious activity flagged
- Audit logs reviewed weekly
Compliance Roadmap
GDPR (EU)
- User consent tracking
- Right to deletion (account deletion)
- Data export functionality
- Privacy policy
- Cookie consent banner
PCI-DSS (Payments)
- Stripe handles card data (we never store cards)
- Encryption at rest and in transit
- Access controls
- Audit logging