5. Seguridad
Conclusión. Una sola identidad para todos los partners (OAuth2 y OIDC con Keycloak), tokens cortos de 5 minutos, autenticación de cliente con llave privada en lugar de secretos compartidos, y autorización en tres niveles: scope por ruta, política por partner y canal, y pertenencia del recurso. El gateway valida el token, la malla lo vuelve a validar y aplica la cuota, y el servicio decide sobre el negocio. La capa no toca datos de tarjeta y guarda los datos personales en un solo servicio, cifrados.
5.1 Flujos OAuth2 y OIDC
| Quién | Flujo | Detalle |
|---|---|---|
| Marketplaces y sistemas de partners | Client Credentials | Máquina a máquina. Un cliente de Keycloak por partner |
| E-commerce, a nombre de un cliente final | Authorization Code con PKCE (Proof Key for Code Exchange, la protección del flujo para aplicaciones que no pueden guardar un secreto) | El backend del e-commerce llama a la capa con su propio cliente; la identidad del comprador viaja como customerId |
| Servicios internos | Sin tokens de usuario | Identidad de carga de trabajo por mTLS (mutual TLS: ambos extremos se autentican con certificado) de la malla |

El partner se autentica con private_key_jwt: firma con su llave privada y la capa conoce solo la pública. Así no hay secreto compartido que robar. Para partners que no puedan, secreto de cliente con rotación obligatoria cada 90 días. Trade-off: más fricción de alta para el partner (supuesto S16), a cambio de eliminar la credencial que más se filtra.
El token es un JWT (JSON Web Token: token firmado que lleva los datos de quien llama, llamados claims):
| Claim | Ejemplo | Uso |
|---|---|---|
iss |
https://auth.andesretail.com/realms/andesretail |
Validación |
aud |
interop-api |
Validación |
exp |
5 minutos | Validación |
azp |
partner-mkp-andino |
Cliente |
partner_id |
mkp-andino |
Aislamiento de datos y cuota |
channels |
["mkp-andino"] |
Canales permitidos |
tier |
gold |
Nivel de cuota |
scope |
stock:read prices:read orders:write orders:read |
Autorización por ruta |
5.2 Qué se valida en el gateway y qué en los servicios
| Validación | Gateway | Sidecar (malla) | Servicio |
|---|---|---|---|
| TLS 1.2 o superior | Sí (borde) | mTLS interno | |
Firma del JWT, iss, aud, exp |
Sí | Sí, de nuevo | |
| Scope exigido por la ruta | Sí | Sí | |
| Tope general y por cliente | Sí | ||
| Cuota por partner y nivel | Sí | ||
| Solo el gateway puede llamar al servicio | Sí (política por identidad de carga) | ||
| Tamaño y tipo de contenido | Sí | ||
| Esquema del cuerpo | Sí | ||
El recurso pertenece al partner_id |
Sí | ||
| Canal, tienda y modo de cobro permitidos | Sí | ||
| Reglas de negocio | Sí |
Un sidecar es un contenedor que acompaña a cada servicio y le aporta capacidades sin tocar su código; aquí es el proxy Envoy de Istio. Por qué validar el token dos veces: si alguien alcanza un servicio sin pasar por el gateway, el servicio no confía en cabeceras sino en el token. Los servicios leen partner_id del token validado por su sidecar, nunca de una cabecera puesta por el cliente. Trade-off: unos milisegundos más por salto (dentro del presupuesto de la sección 6.2) a cambio de no tener un perímetro único.
5.3 Autorización por roles, scopes, partner y canal
- Scope por ruta: qué operaciones puede invocar (
stock:read,prices:read,orders:write,orders:read,webhooks:manage). Ninguna ruta del gateway existe sin scope: lo exige el pipeline (F5). - Política por partner y canal, como configuración versionada: canales, tiendas, listas de precios, modos de cobro, vencimiento de reserva y tope visible de stock.
- Pertenencia del recurso: toda consulta a la base filtra por
partner_id. Una orden ajena responde 404.
Roles internos para operadores de AndesRetail en Keycloak: support-read (consultar órdenes y entregas de webhooks) y ops-admin (reprocesar colas de fallidos, cambiar cuotas). Toda acción de operador queda en auditoría.
5.4 Rotación de llaves, expiración y secretos
| Elemento | Vigencia | Rotación |
|---|---|---|
| Token de acceso | 5 minutos | No se revoca: vence. Sin tokens de refresco en Client Credentials |
| Llaves de firma de Keycloak | 90 días | Con solapamiento: la llave nueva se publica en el JWKS (el documento público de llaves, ver abajo) antes de usarse; la vieja sigue válida hasta que venzan sus tokens |
Llaves de los partners (private_key_jwt) |
12 meses | El partner registra la nueva antes de retirar la vieja |
| Secreto de cliente (si aplica) | 90 días | Dos secretos válidos durante el cambio |
| Secreto de firma de webhooks | 12 meses o a demanda | Doble firma durante el cambio |
| Credenciales hacia ERP, WMS, OMS, CRM, pasarela, Kafka | 90 días | Automática en el gestor de secretos |
| Certificados mTLS de la malla | 24 horas | Automática por Istio |
El JWKS (JSON Web Key Set) es el documento público con las llaves con las que se verifica la firma de los tokens; el gateway y los sidecars lo cachean y lo refrescan ante una llave desconocida.
Estrategia de secretos:
- Fuente: AWS Secrets Manager. Los pods los reciben como archivos montados por el Secrets Store CSI Driver (CSI, Container Storage Interface: la interfaz estándar con la que Kubernetes monta volúmenes externos).
- Acceso a servicios de AWS sin credenciales guardadas: EKS Pod Identity, que asigna un rol de IAM (Identity and Access Management, el sistema de permisos de AWS) al pod.
- Ningún secreto en el repositorio, en imágenes ni en variables de entorno. El pipeline escanea secretos en cada cambio.
- Cada adaptador solo puede leer el secreto de su sistema.
Trade-off: la rotación toma efecto en el siguiente sondeo del driver y solo si el servicio relee el archivo; por eso el contrato de todo servicio exige leer configuración y secretos de archivos. Se descartó Vault de HashiCorp para no sumar un proyecto de operación (alta disponibilidad, desellado, respaldo); el cambio a Vault solo tocaría la configuración del driver, no el servicio.
5.5 Datos personales y de pago
- Minimización: la capa solo recibe lo necesario para entregar la orden (nombre, contacto, dirección). Estos son datos personales o PII (personally identifiable information: datos que identifican a una persona).
- Viven únicamente en order-service, cifrados por campo con llaves de AWS KMS (Key Management Service, el servicio de gestión de llaves de AWS).
- No aparecen en eventos, logs, trazas ni métricas. Los eventos llevan
customerIdyorderId. - Retención: se anonimizan a los 90 días de la entrega, salvo obligación legal distinta. Marco aplicable: Ley 1581 de 2012, la ley colombiana de protección de datos personales (supuesto S10).
- Pago: la capa nunca recibe, procesa ni guarda números de tarjeta. El canal tokeniza con la pasarela y envía el token. Con eso la capa queda fuera del alcance fuerte de PCI DSS (Payment Card Industry Data Security Standard, el estándar de la industria de tarjetas, que exige auditorías costosas a quien toque datos de tarjeta). Si un partner envía un número de tarjeta por error, un detector lo rechaza en el texto libre (nombre, correo, dirección) y en
payment.token, y un filtro de logs lo enmascara. - Los identificadores de la orden no pasan por el detector: tienen formato declarado en el contrato (sección 4.6). Un dígito de verificación acierta por azar una de cada diez veces, así que el detector adivina, y sobre un identificador adivinar rechaza órdenes válidas: un UUID de la tienda caía en él una de cada 450 compras, y el reintento con la misma llave recibía el mismo rechazo (ADR-026). Trade-off: una tarjeta puesta a propósito en un identificador no se detecta. Se acepta porque el detector protege contra el error y no contra el abuso: la capa nunca recibe tarjetas, solo el token de la pasarela. En el texto libre sigue adivinando, y un número de 13 a 19 dígitos en una dirección se rechaza aunque no sea tarjeta; ahí el cliente ve qué campo y lo corrige.
5.6 Modelo de amenazas
Se documentan 9 amenazas; aquí van las 6 principales y las otras 3 en el anexo A.
| # | Amenaza | Mitigación |
|---|---|---|
| A1 | Robo o reutilización de un token | Vigencia de 5 minutos, private_key_jwt, TLS, audiencia fija, detección de uso anómalo por partner |
| A2 | Un partner accede a órdenes o precios de otro (BOLA, broken object level authorization: falla al comprobar que el objeto pedido es de quien lo pide) | Filtro por partner_id en toda consulta, 404 para recursos ajenos, pruebas automáticas de aislamiento entre partners (F7) |
| A3 | Webhook de pago falsificado ("pago aprobado" inventado) | Implementado: verificación de firma de la pasarela, deduplicación, y confirmación contra la API de la pasarela antes de confirmar la orden. Diseño, sin implementar en el demo: lista de IP de origen |
| A4 | Abuso o denegación de servicio: scraping de precios, ráfagas | Cuota por partner y nivel, tope general, microcache, Cloudflare en el borde, protección del legado por diseño |
| A5 | Órdenes duplicadas o repetición de peticiones | Idempotencia con dos barreras, deduplicación de eventos (F9) |
| A6 | Fuga de datos personales por logs, eventos o trazas | Sin datos personales en eventos, enmascarado en logs, cifrado por campo, pruebas que fallan si aparece un dato personal en la telemetría (F8) |
5.7 Auditoría
Diseño: toda petición externa de escritura, todo cambio de estado de orden, reserva y pago, toda acción de operador y todo fallo de autenticación o autorización genera un registro en el tópico andes.audit.v1: quién (partner_id, cliente o operador), qué, cuándo, resultado y correlationId, y se archiva en almacenamiento de objetos con bloqueo contra modificación.
Implementado hoy: order-service publica el registro, con ese contenido y por outbox, en cuatro acciones (crear orden, confirmarla, reembolsar el pago y cancelarla). Todavía no auditan inventory-service (reservas), payment-service (pagos), webhook-service (alta y baja de suscripciones) ni el borde (fallos de autenticación o autorización), y el archivo con bloqueo contra modificación no está construido. Completar esos tres productores y el archivo es trabajo del roadmap (sección 8.3). Trade-off: costo de almacenamiento que crece sin límite, a cambio de poder reconstruir cualquier disputa con un partner.
5.8 Acceso de los operadores al clúster
La API de Kubernetes de EKS es solo privada: no tiene dirección pública, ni siquiera limitada a una IP. Se entra por un túnel de AWS Systems Manager (SSM, el servicio de AWS para administrar instancias) hacia un bastión: una máquina mínima (t4g.nano) dentro de la red privada, sin llave SSH, sin IP pública y sin reglas de entrada, con un rol que solo le permite hablar con SSM. El bastión no tiene permisos sobre Kubernetes: solo reenvía bytes, y quien se autentica ante la API es kubectl, en el equipo del operador, con su propio rol. Hay dos contextos: administrador (abre el túnel y despliega) y solo lectura, que ve todo menos los Secrets de Kubernetes y es el que se muestra en vivo. La verificación del certificado de la API se mantiene (el cliente valida el nombre del endpoint de EKS aunque se conecte a localhost); nunca se desactiva. CloudTrail registra quién abrió cada sesión.
Se descartaron el endpoint público limitado por IP (la API de un clúster con secretos expuesta a internet aunque esté filtrada, y una variable que cambiar cada vez que cambia la red del operador), el EC2 Instance Connect Endpoint (túneles de una hora como máximo y apuntados a una IP que en EKS cambia) y AWS Client VPN (veinte veces el costo del bastión, con certificados que administrar). Trade-off: un recurso más que cobra por hora (unos 0,005 dólares), que depende del NAT y que puede fallar; todo lo que habla con la API necesita el túnel abierto. Como respaldo, y solo por decisión explícita, el endpoint público limitado a una IP se vuelve a encender con una variable (nunca abierto a todo internet). Fuente: ADR-027, que reemplaza ese punto de ADR-022.