Mejores Prácticas de Seguridad
Directrices de seguridad esenciales para implementaciones en producción.
Seguridad de Credenciales
Almacenamiento de API Keys
Nunca hacer:
- ❌ Codificar credenciales en código fuente
- ❌ Incluir credenciales en repositorios Git
- ❌ Compartir credenciales por email o chat
- ❌ Usar mismas credenciales en múltiples entornos
- ❌ Registrar credenciales en logs
Siempre hacer:
- ✅ Usar variables de entorno
- ✅ Usar servicios de gestión de secretos (AWS Secrets Manager, Azure Key Vault)
- ✅ Rotar credenciales regularmente
- ✅ Usar credenciales diferentes para sandbox y producción
- ✅ Limitar acceso a credenciales (principio de menor privilegio)
Implementación
// .env (NUNCA commitear este archivo)
KOYWE_API_KEY=tu_api_key_aqui
KOYWE_SECRET=tu_secret_aqui
KOYWE_ORG_ID=tu_org_id
KOYWE_MERCHANT_ID=tu_merchant_id
// app.js
require('dotenv').config();
const koyweConfig = {
apiKey: process.env.KOYWE_API_KEY,
secret: process.env.KOYWE_SECRET,
orgId: process.env.KOYWE_ORG_ID,
merchantId: process.env.KOYWE_MERCHANT_ID
};
// Validar que existen
if (!koyweConfig.apiKey || !koyweConfig.secret) {
throw new Error('Credenciales Koywe faltantes');
}import os
from dotenv import load_dotenv
load_dotenv()
KOYWE_CONFIG = {
'api_key': os.getenv('KOYWE_API_KEY'),
'secret': os.getenv('KOYWE_SECRET'),
'org_id': os.getenv('KOYWE_ORG_ID'),
'merchant_id': os.getenv('KOYWE_MERCHANT_ID')
}
# Validar
if not all(KOYWE_CONFIG.values()):
raise ValueError('Credenciales Koywe faltantes')Rotación de Secretos API
La rotación regular de secretos es una práctica de seguridad crítica. Rota tus secretos API:
- Por calendario: Se recomienda rotación trimestral
- Después de eventos de seguridad: Compromiso sospechado, desvinculación de empleados
- Por cumplimiento: Muchos marcos de seguridad requieren rotación periódica
Endpoint de Rotación
Endpoint: POST /api/v1/auth/credentials/rotate-secret
Autenticación: Se requiere token Bearer
Restricción: Solo usuarios API pueden rotar secretos (no usuarios autenticados por email)
Cómo Rotar
async function rotarSecretoApi(tokenActual) {
// Paso 1: Llamar endpoint de rotación con token actual
const response = await axios.post(
'https://api.koywe.com/api/v1/auth/credentials/rotate-secret',
{},
{
headers: { 'Authorization': `Bearer ${tokenActual}` }
}
);
const nuevoSecreto = response.data.secret;
console.log('Nuevo secreto generado (¡almacenar inmediatamente!)');
// Paso 2: Almacenar nuevo secreto en tu gestor de secretos
await secretManager.update('KOYWE_SECRET', nuevoSecreto);
// Paso 3: Verificar que el nuevo secreto funciona
const verifyResponse = await axios.post(
'https://api.koywe.com/api/v1/auth/sign-in',
{
apiKey: process.env.KOYWE_API_KEY,
secret: nuevoSecreto
}
);
if (verifyResponse.data.token) {
console.log('✓ Nuevo secreto verificado exitosamente');
// Paso 4: Eliminar secreto antiguo del vault
await secretManager.delete('KOYWE_SECRET_OLD');
}
return nuevoSecreto;
}def rotar_secreto_api(token_actual):
# Paso 1: Llamar endpoint de rotación con token actual
response = requests.post(
'https://api.koywe.com/api/v1/auth/credentials/rotate-secret',
headers={'Authorization': f'Bearer {token_actual}'}
)
nuevo_secreto = response.json()['secret']
print('Nuevo secreto generado (¡almacenar inmediatamente!)')
# Paso 2: Almacenar nuevo secreto en tu gestor de secretos
secret_manager.update('KOYWE_SECRET', nuevo_secreto)
# Paso 3: Verificar que el nuevo secreto funciona
verify_response = requests.post(
'https://api.koywe.com/api/v1/auth/sign-in',
json={
'apiKey': os.environ['KOYWE_API_KEY'],
'secret': nuevo_secreto
}
)
if verify_response.json().get('token'):
print('✓ Nuevo secreto verificado exitosamente')
# Paso 4: Eliminar secreto antiguo del vault
secret_manager.delete('KOYWE_SECRET_OLD')
return nuevo_secreto# Rotar secreto
curl -X POST 'https://api.koywe.com/api/v1/auth/credentials/rotate-secret' \
-H 'Authorization: Bearer TU_TOKEN_ACTUAL'
# Respuesta: { "secret": "2af81190b3a153r48a3df3a1eefcc386ca763b99fba53d39666751ffd4e2ae81" }Importante: El nuevo secreto solo se muestra una vez en la respuesta. Almacénalo inmediatamente en tu gestor de secretos antes de descartar el secreto antiguo.
Mejores Prácticas de Rotación
Lista de Verificación de Rotación:
- Respaldar secreto actual antes de rotar
- Almacenar nuevo secreto en gestor de secretos inmediatamente
- Verificar que el nuevo secreto funciona antes de eliminar el antiguo
- Actualizar todas las aplicaciones/servicios que usan el secreto
- Documentar la rotación para auditoría
Manejo de Errores
async function rotarSecretoConReintentos(tokenActual, maxReintentos = 3) {
for (let intento = 1; intento <= maxReintentos; intento++) {
try {
const nuevoSecreto = await rotarSecretoApi(tokenActual);
return nuevoSecreto;
} catch (error) {
console.error(`Intento ${intento} falló:`, error.message);
if (error.response?.status === 401) {
throw new Error('Token inválido - no se puede rotar');
}
if (intento === maxReintentos) {
throw new Error('Rotación de secreto falló después de múltiples intentos');
}
// Esperar antes de reintentar
await new Promise(resolve => setTimeout(resolve, 1000 * intento));
}
}
}Seguridad de Webhooks
Verificar Firmas
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// payload DEBE ser el body crudo (Buffer o string)
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Comparación tiempo-constante para prevenir timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express - usar express.raw
app.post('/webhooks/koywe',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['koywe-signature'];
const secret = process.env.KOYWE_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).send('Firma inválida');
}
// Procesar webhook...
res.status(200).send('OK');
}
);Proteger Endpoint
const rateLimit = require('express-rate-limit');
// Limitar webhook requests
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minuto
max: 100, // máximo 100 requests por minuto
message: 'Demasiadas solicitudes, intenta más tarde'
});
app.post('/webhooks/koywe',
webhookLimiter,
express.raw({ type: 'application/json' }),
handleWebhook
);Seguridad de Transporte
Usar HTTPS
Requisito de Producción: Todos los endpoints de webhook DEBEN usar HTTPS en producción.
app.post('/webhooks/koywe', (req, res) => {
// Rechazar requests no HTTPS en producción
if (process.env.NODE_ENV === 'production' && req.protocol !== 'https') {
return res.status(403).send('HTTPS requerido');
}
// Procesar webhook...
});Seguridad de Datos
Redactar Información Sensible
function sanitizeForLogging(data) {
const sensitiveFields = [
'apiKey',
'secret',
'token',
'password',
'accountNumber',
'cardNumber',
'cvv'
];
const sanitized = { ...data };
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '***REDACTED***';
}
}
return sanitized;
}
// Uso
console.log('Orden creada:', sanitizeForLogging(orderData));Encriptar Datos Sensibles
const crypto = require('crypto');
class SecureStorage {
constructor(encryptionKey) {
this.algorithm = 'aes-256-gcm';
this.key = Buffer.from(encryptionKey, 'hex');
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encrypted, iv, authTag) {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Uso
const storage = new SecureStorage(process.env.ENCRYPTION_KEY);
const encrypted = storage.encrypt('datos-sensibles');Validación de Entrada
Sanitizar Datos de Usuario
const validator = require('validator');
function validateOrderInput(orderData) {
const errors = [];
// Validar amount
if (!orderData.amount || typeof orderData.amount !== 'number') {
errors.push('Monto inválido');
}
if (orderData.amount <= 0 || orderData.amount > 10000000) {
errors.push('Monto fuera de rango');
}
// Validar currency
const validCurrencies = ['COP', 'USD', 'BRL', 'MXN', 'CLP'];
if (!validCurrencies.includes(orderData.currency)) {
errors.push('Moneda inválida');
}
// Validar email si existe
if (orderData.email && !validator.isEmail(orderData.email)) {
errors.push('Email inválido');
}
// Prevenir inyección en externalId
if (orderData.externalId) {
orderData.externalId = validator.escape(orderData.externalId);
}
if (errors.length > 0) {
throw new Error(`Validación fallida: ${errors.join(', ')}`);
}
return orderData;
}Control de Acceso
Implementar RBAC
const permissions = {
'admin': ['read', 'write', 'delete'],
'operator': ['read', 'write'],
'viewer': ['read']
};
function requirePermission(permission) {
return (req, res, next) => {
const userRole = req.user.role;
const userPermissions = permissions[userRole] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({
error: 'Permisos insuficientes'
});
}
next();
};
}
// Uso
app.post('/api/orders',
authenticate,
requirePermission('write'),
createOrderHandler
);Auditoría y Monitoreo
Registrar Eventos de Seguridad
class SecurityAuditLogger {
static async log(event, details) {
const auditEntry = {
timestamp: new Date().toISOString(),
event,
userId: details.userId,
ip: details.ip,
userAgent: details.userAgent,
action: details.action,
resource: details.resource,
success: details.success,
reason: details.reason
};
// Registrar en sistema seguro
await db.query(
`INSERT INTO security_audit_log
(timestamp, event, user_id, ip, action, success)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
auditEntry.timestamp,
auditEntry.event,
auditEntry.userId,
auditEntry.ip,
auditEntry.action,
auditEntry.success
]
);
}
}
// Uso
await SecurityAuditLogger.log('AUTHENTICATION_ATTEMPT', {
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent'],
action: 'login',
success: true
});Lista de Verificación de Seguridad
Antes de Producción
- Credenciales almacenadas en variables de entorno/secrets manager
- HTTPS habilitado en todos los endpoints
- Verificación de firma de webhook implementada
- Límite de tasa configurado
- Logging de seguridad habilitado
- Datos sensibles redactados en logs
- Validación de entrada implementada
- Encripción en reposo para datos sensibles
- Monitoreo y alertas configurados
- Plan de respuesta a incidentes documentado
Respuesta a Incidentes
Plan de Respuesta
- Detectar: Monitoreo y alertas automatizadas
- Contener: Revocar credenciales comprometidas
- Erradicar: Identificar y corregir vulnerabilidad
- Recuperar: Restaurar servicio normal
- Lecciones Aprendidas: Documentar y mejorar
async function emergencyCredentialRevocation() {
// 1. Deshabilitar API keys comprometidas
await disableApiKey(compromisedApiKey);
// 2. Generar nuevas credenciales
const newCredentials = await generateNewApiKey();
// 3. Notificar equipo
await sendSecurityAlert({
type: 'CREDENTIAL_COMPROMISED',
action: 'REVOKED_AND_REGENERATED',
newKeyId: newCredentials.id
});
// 4. Actualizar sistemas
await updateProductionCredentials(newCredentials);
return newCredentials;
}Cumplimiento
GDPR / Protección de Datos
Requisitos de Retención de Datos:
- Almacenar solo datos necesarios
- Implementar derecho al olvido
- Proporcionar exportación de datos
- Documentar procesamiento de datos
Próximos Pasos
Last updated on