Documentación completa del modelo de seguridad de Cortex Knowledge Assistant, incluyendo autenticación, autorización, protección de datos y mejores prácticas para producción.
Cortex implementa un modelo de defensa en profundidad:
- Autenticación: JWT tokens con expiración y refresh
- Autorización: RBAC (Role-Based Access Control) + multi-tenancy
- Protección de Datos: DLP/PII redaction automático
- Rate Limiting: Protección contra abuso
- Headers de Seguridad: HSTS, CSP, X-Frame-Options
- Auditoría: Log inmutable de operaciones sensibles
Cortex utiliza JWT Bearer tokens para autenticar todas las requests.
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Características del Token:
- Algoritmo: HS256
- Expiración: 24 horas (configurable)
- Claims incluidos:
user_id,username,user_type,role,dlp_level,subject_ids,can_access_all_subjects
┌─────────────────────────────────────────────────────────────────────────────┐
│ FLUJO DE AUTENTICACIÓN │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────┐ POST /auth/login ┌──────────────┐
│ Cliente │ ──────────────────────────────▶ │ Cortex API │
│ │ {username, password} │ │
└──────────┘ └──────┬───────┘
│
▼
┌─────────────────┐
│ 1. Buscar user │
│ 2. Verify hash │
│ 3. Check status │
│ 4. Load subjects│
│ 5. Issue JWT │
└─────────┬───────┘
│
┌──────────┐ 200 OK │
│ Cliente │ ◀──────────────────────────────────────┘
│ │ {access_token, user}
└──────────┘
Requests subsiguientes:
┌──────────┐ Authorization: Bearer xxx ┌──────────────┐
│ Cliente │ ──────────────────────────────▶ │ Cortex API │
│ │ GET /query │ │
└──────────┘ └──────┬───────┘
│
▼
┌─────────────────┐
│ 1. Decode JWT │
│ 2. Verify sig │
│ 3. Check expiry │
│ 4. Build context│
└─────────────────┘
- Hashing: bcrypt con cost factor 12
- Validación: Mínimo 12 caracteres para admin
- Timing-safe comparison: Evita timing attacks en login
# Nunca se almacenan passwords en texto plano
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(12))# Clave secreta para firmar tokens (OBLIGATORIO cambiar en producción)
JWT_SECRET_KEY=your-super-secret-key-at-least-32-characters
# Expiración del token (por defecto 24h)
JWT_EXPIRATION_HOURS=24| Role | user_type | Permisos |
|---|---|---|
admin |
employee |
Acceso total, endpoints /admin/*, gestión de usuarios |
user |
employee |
Consultas RAG, acceso a subjects asignados |
customer |
customer |
Solo sus propios datos, sin acceso a /admin/* |
El sistema aísla datos por "tenant" usando el concepto de Subject:
┌─────────────────────────────────────────────────────────────────────────────┐
│ MODELO MULTI-TENANT │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────┐
│ ADMIN │
│ can_access_all_subjects=true │
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ Todos los Subjects │
│ CLI-001, CLI-002, CLI-003, ... │
└───────────────────────────────────┘
┌─────────────────────────┐ ┌─────────────────────────┐
│ EMPLOYEE │ │ CUSTOMER │
│ can_access_all=false │ │ │
│ subject_ids=[CLI-001] │ │ subject_ids=[CLI-002] │
└───────────┬─────────────┘ └───────────┬─────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ CLI-001 │ │ CLI-002 │
│ (Solo) │ │ (Solo) │
└───────────────┘ └───────────────┘
# Simplificado de api/main.py
if user_type == "employee":
if can_access_all_subjects:
# Admin: puede consultar cualquier subject o docs públicos
subject_id = requested_subject # None = solo docs públicos
else:
# Empleado limitado: solo sus subjects asignados
if requested_subject in allowed_subject_ids:
subject_id = requested_subject
else:
subject_id = allowed_subject_ids[0]
else:
# Customer: SIEMPRE restringido a sus subjects
if not allowed_subject_ids:
raise HTTPException(403, "No customer scope assigned")
subject_id = allowed_subject_ids[0]El retriever filtra documentos por subject_id:
# Filtro aplicado en retriever_qdrant.py
filter = {
"should": [
{"key": "subject_id", "match": {"value": subject_id}},
{"is_null": {"key": "subject_id"}} # Docs públicos
]
}El sistema aplica redacción automática de información sensible antes de enviar respuestas al cliente.
Pipeline DLP:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ LLM Answer │────▶│ DLP Engine │────▶│ Cliente │
│ (Raw) │ │ (Redact) │ │ (Sanitized) │
└──────────────┘ └──────────────┘ └──────────────┘
| Tipo | Patrón | Reemplazo |
|---|---|---|
| DNI/ID Nacional | \d{7,9} |
<dni-redacted> |
| CUIT/CUIL | \d{2}-\d{7,8}-\d |
<cuit-redacted> |
| Tarjetas | \d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4} |
<card-redacted> |
| Pattern estándar | <email-redacted> |
|
| Teléfono | \+?\d[\d\s\-]{6,}\d |
<phone-redacted> |
Antes (respuesta del LLM):
El cliente Juan García con DNI 12345678 tiene la tarjeta 4532-1234-5678-9012
y puede contactarlo en juan@email.com o al +54 11 4567-8901.
Después (enviado al cliente):
El cliente Juan García con DNI <dni-redacted> tiene la tarjeta <card-redacted>
y puede contactarlo en <email-redacted> o al <phone-redacted>.
| dlp_level | Comportamiento |
|---|---|
standard |
Redacción completa de PII (default) |
privileged |
Sin redacción (solo para backoffice interno) |
Durante la ingesta de documentos, cada chunk es clasificado por sensibilidad:
class PiiClassification:
has_pii: bool
by_type: Dict[str, bool] # dni, cuit, card, phone, email
sensitivity: Literal["none", "low", "medium", "high"]Política de Sensibilidad:
high: Tarjetas de crédito O múltiples tipos de PIImedium: Un identificador fuerte (DNI, CUIT, email, teléfono)low/none: Sin PII detectado
Antes de incluir datos personales en el prompt del LLM, el sistema aplica enmascaramiento basado en el rol del usuario que realiza la consulta.
Ubicación: src/cortex_ka/application/pii_masking.py
Flujo de Enmascaramiento:
┌──────────────────────────────────────────────────────────────────────────────┐
│ ENMASCARAMIENTO PII PARA LLM │
└──────────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CustomerSnapshot│────▶│ mask_snapshot │────▶│ Prompt Builder │
│ (datos raw) │ │ (por rol) │ │ (datos safe) │
└─────────────────┘ └────────┬────────┘ └─────────────────┘
│
┌────────────┴────────────┐
│ Reglas por viewer_role │
└────────────┬────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ admin │ │ support │ │ customer │
│ Full data │ │ Parcial │ │ Mínimo │
└───────────┘ └───────────┘ └───────────┘
Matriz de Visibilidad por Rol:
| Campo | Admin | Support | Customer |
|---|---|---|---|
dni |
Completo | Últimos 4 | Últimos 4 |
email |
Completo | Parcial (d***@...) | Solo dominio |
phone |
Completo | Últimos 4 | Solo código país |
address |
Completo | Solo ciudad | Solo ciudad |
accounts |
Completo | Completo | Solo propias |
full_name |
Completo | Completo | Completo |
Ejemplo de Enmascaramiento:
# Datos originales en CustomerSnapshot
{
"dni": "12345678",
"email": "juan.perez@email.com",
"phone": "+54 11 4567-8901"
}
# Vista para rol 'support'
{
"dni": "****5678",
"email": "j***@email.com",
"phone": "****-8901"
}
# Vista para rol 'customer'
{
"dni": "****5678",
"email": "***@email.com",
"phone": "+54 ****"
}Implementación:
from cortex_ka.application.pii_masking import mask_snapshot_for_role
# En rag_service.py durante construcción de contexto
masked = mask_snapshot_for_role(customer_snapshot, viewer_role=current_user.role)
prompt = build_prompt(query, context, masked_snapshot=masked)Auditoría:
Todo acceso a datos PII se registra con:
viewer_role: Rol del usuario que accedesubject_id: ID del subject consultadopii_fields_accessed: Campos PII incluidos en el contextomasking_applied: Nivel de enmascaramiento aplicado
Protección contra abuso y DDoS.
CKA_RATE_LIMIT_QPM=120 # Queries por minuto
CKA_RATE_LIMIT_BURST=10 # Capacidad de burst
CKA_RATE_LIMIT_WINDOW_SECONDS=60 # Ventana de tiempo- Algoritmo: Token bucket con sliding window
- Key: Por
user_idoapi_key - Storage: Redis (producción) o in-memory (desarrollo)
HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/json
{"detail": "rate_limited"}X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
X-Request-ID: abc123def456Con CKA_HTTPS_ENABLED=true:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'CKA_CSP_POLICY=default-src 'self'; script-src 'self' https://trusted-cdn.comTodas las operaciones sensibles se registran en un log de auditoría inmutable.
| Operación | Descripción |
|---|---|
login |
Intento de login (exitoso o fallido) |
query |
Consulta RAG realizada |
admin_create_user |
Creación de usuario |
admin_update_user |
Modificación de usuario |
admin_delete_user |
Eliminación de usuario |
admin_create_product |
Creación de producto |
admin_create_transaction |
Creación de transacción |
admin_update_subject_data |
Modificación de datos de cliente |
admin_load_demo_transactions |
Carga de datos demo |
refresh_public_docs |
Re-ingesta de documentos |
{
"id": 456,
"user_id": "1",
"username": "admin",
"subject_key": "CLI-81093",
"operation": "query",
"outcome": "success",
"details": {
"duration_sec": 1.23,
"query": "¿Cuál es mi saldo?"
},
"created_at": "2024-12-27T15:30:00Z"
}curl -X GET "http://localhost:8088/admin/audit-log?limit=100&operation=login" \
-H "Authorization: Bearer $ADMIN_TOKEN"CKA_CORS_ORIGINS=https://cortex.example.com,https://admin.example.comProducción: Siempre especificar orígenes explícitos, nunca *.
En producción, solo exponer:
- Puerto
443(HTTPS) - vía reverse proxy
NO exponer directamente:
:8088(API):6333(Qdrant):5432(PostgreSQL):6379(Redis)
# k8s/networkpolicies/cortex-api.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cortex-api
spec:
podSelector:
matchLabels:
app: cortex-api
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- port: 8088
egress:
- to:
- podSelector:
matchLabels:
app: qdrant
ports:
- port: 6333
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- port: 5432
- to:
- podSelector:
matchLabels:
app: redis
ports:
- port: 6379# .env (NO commitear)
JWT_SECRET_KEY=dev-secret-key
HF_API_KEY=hf_xxxx
POSTGRES_PASSWORD=dev_passwordUsar gestores de secretos:
Docker Swarm:
secrets:
jwt_secret:
external: trueKubernetes:
apiVersion: v1
kind: Secret
metadata:
name: cortex-secrets
data:
JWT_SECRET_KEY: base64_encoded_value
HF_API_KEY: base64_encoded_valueVault/AWS Secrets Manager: Integrar vía init container o sidecar.
- Generar
JWT_SECRET_KEYúnico (mín 32 chars) - Cambiar
POSTGRES_PASSWORDdel default - Configurar
CKA_CORS_ORIGINScon orígenes específicos - Habilitar
CKA_HTTPS_ENABLED=true - Deshabilitar
CKA_ENABLE_DEMO_API_KEY=false - Configurar
CKA_CONFIDENTIAL_RETRIEVAL_ONLY=truesi aplica - Revisar y ajustar
CKA_CSP_POLICY
- Usar HTTPS con certificados válidos
- Configurar reverse proxy (nginx/traefik)
- No exponer puertos internos públicamente
- Configurar firewall/security groups
- Habilitar Network Policies en Kubernetes
- Configurar alertas para errores 401/403 excesivos
- Monitorear rate limiting (429s)
- Revisar audit log periódicamente
- Configurar log rotation
- Backup automático de PostgreSQL
- Backup de Qdrant (snapshots)
- Test de restauración periódico
- Identificar el
user_idafectado - Deshabilitar usuario:
PUT /api/admin/users/{id}constatus: inactive - Rotar
JWT_SECRET_KEY(invalida TODOS los tokens) - Revisar audit log para actividad sospechosa
- Revisar audit log:
operation=login, outcome=failure - Identificar IP/usuario atacado
- Considerar bloqueo temporal a nivel de WAF/firewall
- Incrementar
CKA_RATE_LIMIT_QPMsi es necesario
- Identificar alcance vía audit log
- Verificar que DLP estuvo activo (
CKA_DLP_ENABLED=true) - Notificar según regulación (GDPR, CCPA, etc.)
- Revisar logs de acceso a subjects afectados