Las tres lecciones anteriores han asegurado la identidad, el canal y el código. Todo eso se ejecuta encima de una plataforma —imágenes, pods, secretos, red del clúster, permisos del API server, pipelines— y un fallo en esa capa deja sin efecto las demás: una imagen con una biblioteca vulnerable, un contenedor que corre como root y escapa al nodo, un Secret legible por cualquiera del namespace, un pod que puede hablar con quien quiera, o un token de CI con permisos de cluster-admin. Esta lección cierra el módulo endureciendo la plataforma de TechCorp: modelo de amenazas y las 4C, imágenes seguras (escaneo con Trivy, firma con cosign, digests), securityContext completo y Pod Security Admission, secretos (Kubernetes Secrets, External Secrets Operator, rotación, OIDC en CI), NetworkPolicy con deny-all por defecto (cerrando lo que 07-02 remitió aquí), RBAC y ServiceAccount por servicio, auditoría y detección, cadena de suministro, y una lista de comprobación con el estado real de TechCorp. Autenticación de usuarios (07-01), TLS/mTLS (07-02) y OWASP/código (07-03) solo se enlazan.
Aviso. Las políticas, versiones y valores de esta lección son didácticos. Antes de aplicarlos en un clúster real, revísalos con un profesional de seguridad de plataforma y con quien responda del cumplimiento; y prueba cada restricción en staging, porque muchas rompen cargas de trabajo que hasta ahora funcionaban.
Contenido
- Modelo de amenazas de la plataforma y las 4C
- Imágenes seguras: base mínima, escaneo, firma y digests
- Seguridad del pod:
securityContextcompleto y Pod Security Admission - Secretos: Kubernetes Secrets, External Secrets Operator y rotación
- Secretos en CI: GitHub Environments y OIDC
- Red:
NetworkPolicycon deny-all por defecto - RBAC de Kubernetes:
ServiceAccountpor servicio y roles mínimos - Auditoría y detección
- Cadena de suministro: lockfile, SBOM y SLSA
- Lista de comprobación de plataforma para TechCorp
- Modelo de amenazas de la plataforma y las 4C
Antes de endurecer nada, qué puede salir mal y en qué capa:
| Amenaza | Cómo ocurre | Capa | Medida en esta lección |
|---|---|---|---|
| Imagen comprometida | Dependencia con CVE, imagen base vieja, paquete malicioso en npm | Container | Base mínima, Trivy en CI, firma cosign, digest |
| Contenedor que escapa | Proceso como root + kernel vulnerable o capabilities de más | Container / Cluster | securityContext restrictivo, seccomp, Pod Security restricted |
| Secreto filtrado | kubectl get secret por alguien de más, etcd sin cifrar, secreto en un log o en git |
Cluster / Code | RBAC de lectura, cifrado en etcd, External Secrets, rotación, gitleaks (07-03) |
| Pod que habla con quien no debe | Sin NetworkPolicy, cualquier pod alcanza cualquier puerto |
Cluster | Deny-all + reglas explícitas |
| Credenciales de CI robadas | Token de larga duración en un secreto de GitHub | Cloud / Cluster | OIDC, Environments, GitOps sin kubeconfig fuera |
| RBAC excesivo | cluster-admin para el pipeline o para Argo CD "porque así funciona" |
Cluster | Roles mínimos por namespace |
| Nube mal configurada | Nodos con IP pública, API server abierto, buckets públicos | Cloud | Responsabilidad de Plataforma/proveedor; fuera del alcance detallado |
Las 4C de la seguridad cloud native (Cloud, Cluster, Container, Code) recuerdan que cada capa hereda la seguridad de la exterior: el mejor securityContext no salva un clúster con el API server abierto a Internet, y el mejor clúster no salva un servicio-pedidos con inyección SQL. Esta lección cubre Container y Cluster; Code fue 07-01/07-03; Cloud es del proveedor y de Plataforma.
- Imágenes seguras: base mínima, escaneo, firma y digests
El Dockerfile de 05-01 ya cumple lo esencial: node:20-alpine, multi-stage (nada de compiladores ni devDependencies en la imagen final), USER node, COPY --chown, HEALTHCHECK y npm ci --omit=dev con el .npmrc como secreto de build. Lo que se añade:
- Base mínima:
alpinereduce la superficie a unos pocos MB; una alternativa aún menor es distroless (gcr.io/distroless/nodejs20-debian12: sin shell, sin gestor de paquetes;kubectl exec ... shdeja de funcionar, lo que es una ventaja de seguridad y un coste de depuración —se resuelve conkubectl debugy contenedores efímeros—). TechCorp mantienealpineen la fase 1 y evalúa distroless para Pagos. - Escaneo en CI con Trivy, que 05-03 dejó configurado con
severity: CRITICAL. La política definitiva: fallar con CRITICAL y HIGH que tengan parche disponible, ignorar las que no lo tienen pero registrarlas, y volver a escanear las imágenes ya desplegadas cada noche (las CVE aparecen después de construir):
# .github/workflows/servicio-node-ci.yml (fragmento, sustituye al de 05-03)
- name: Escaneo de vulnerabilidades
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.IMAGEN }}:sha-${{ github.sha }}
severity: CRITICAL,HIGH
ignore-unfixed: true # sin parche disponible → aviso, no bloqueo (se revisa semanalmente)
exit-code: "1"
format: sarif
output: trivy.sarif
- uses: github/codeql-action/upload-sarif@v3 # los hallazgos aparecen en la pestaña Security del repo
with: { sarif_file: trivy.sarif }- Firma de imágenes con cosign (Sigstore): tras el push, el pipeline firma la imagen con la identidad OIDC del workflow (sin claves que guardar, keyless), y el clúster verifica la firma antes de arrancar un pod, con un controlador de admisión (Kyverno o el policy controller de Sigstore). Solo mención con los comandos:
# En CI (permissions: id-token: write), tras docker/build-push-action:
cosign sign --yes ghcr.io/techcorp/servicio-pedidos@sha256:9f1c… # firma por digest, keyless (Fulcio + Rekor)
# En el clúster o a mano, verificar que la firmó el workflow de TechCorp:
cosign verify ghcr.io/techcorp/servicio-pedidos@sha256:9f1c… \
--certificate-identity-regexp 'https://github.com/techcorp/.*/.github/workflows/servicio-node-ci.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comimagePullPolicyy digest: una etiqueta (1.0.1) puede reapuntarse; un digest (@sha256:…) no. En producción, el overlay de Kustomize fijanewTagydigest(kustomize edit set image ghcr.io/techcorp/servicio-pedidos@sha256:9f1c…),imagePullPolicy: IfNotPresentes seguro porque el digest es inmutable, y el registroghcr.io/techcorpes privado conimagePullSecrets(o identidad de nube) en elServiceAccount.latestsigue prohibido (05-01).
- Seguridad del pod:
securityContext completo y Pod Security Admission
securityContext completo y Pod Security AdmissionEl Deployment de 05-02 tenía runAsNonRoot, runAsUser: 1000, allowPrivilegeEscalation: false y readOnlyRootFilesystem: true. La versión completa, la que exige el perfil restricted:
# k8s/servicio-pedidos/base/deployment.yaml (fragmento spec.template.spec)
spec:
serviceAccountName: servicio-pedidos # apartado 7: identidad propia, sin token montado
automountServiceAccountToken: false
securityContext: # a nivel de pod: se hereda por todos los contenedores
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000 # los volúmenes montados pertenecen al grupo 1000 (secretos como ficheros, apartado 4)
seccompProfile: { type: RuntimeDefault } # filtro de syscalls del runtime (bloquea las peligrosas y raras)
containers:
- name: servicio-pedidos
image: ghcr.io/techcorp/servicio-pedidos@sha256:9f1c… # digest en prod (apartado 2)
securityContext:
allowPrivilegeEscalation: false # ni setuid ni ganar capabilities
readOnlyRootFilesystem: true # el sistema de ficheros de la imagen es de solo lectura
capabilities: { drop: [ALL] } # ninguna capability de Linux (Node no necesita ninguna en 3002 > 1024)
volumeMounts:
- { name: tmp, mountPath: /tmp } # lo único escribible: temporal, vacío en cada arranque
resources: # también es seguridad: un contenedor sin límite puede agotar el nodo (05-02, 06-04)
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
volumes:
- { name: tmp, emptyDir: { sizeLimit: 64Mi } }Qué aporta cada línea nueva: seccompProfile: RuntimeDefault activa el perfil seccomp del runtime (containerd) que bloquea decenas de syscalls que ninguna aplicación normal usa y que son la vía de muchas fugas del contenedor; capabilities: drop: [ALL] quita hasta las capabilities que Docker concede por defecto (NET_RAW, CHOWN...): si un atacante ejecuta código dentro, no puede abrir sockets raw ni cambiar propietarios; emptyDir para /tmp hace compatible readOnlyRootFilesystem con librerías que escriben temporales (con sizeLimit para que no llene el nodo); fsGroup permite leer secretos montados como ficheros sin ser root.
Pod Security Admission (PSA) hace que el clúster rechace pods que no cumplan un perfil, en vez de confiar en que cada Deployment lo escriba bien. Tres perfiles: privileged (todo), baseline (sin lo peor: privilegiado, hostPath, hostNetwork), restricted (todo lo anterior más runAsNonRoot, drop ALL, seccomp, sin escalada). Se activa con etiquetas en el namespace:
# k8s/namespace.yaml (amplía el de 05-02)
apiVersion: v1
kind: Namespace
metadata:
name: techcorp
labels:
pod-security.kubernetes.io/enforce: restricted # rechaza pods que no cumplan
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/warn: restricted # avisa en kubectl apply
pod-security.kubernetes.io/audit: restricted # anota en el audit log (apartado 8)kubectl label ns techcorp pod-security.kubernetes.io/warn=restricted --overwrite # primero solo warn: ver qué se rompería
kubectl apply -k k8s/servicio-pedidos/overlays/dev # "Warning: would violate PodSecurity restricted: ..." si falta algoEl Job de migraciones de 05-02, el gateway, RabbitMQ y Keycloak deben cumplirlo también (o vivir en otro namespace con baseline justificado). Para reglas más finas que las de PSA (exigir digest, prohibir latest, exigir resources), se usa un motor de políticas como Kyverno u OPA Gatekeeper; mención.
- Secretos: Kubernetes Secrets, External Secrets Operator y rotación
Un Secret de Kubernetes (05-02: pedidos-db, pedidos-rabbitmq, y desde 07-01 pedidos-oidc; pagos-pasarela en 07-02) es base64, no cifrado: kubectl get secret pedidos-db -o jsonpath='{.data.PEDIDOS_DB_URL}' | base64 -d lo muestra a cualquiera con permiso de lectura. Tres capas para que sea seguro:
- Cifrado en reposo en etcd (
EncryptionConfigurationdel API server conaescbc/kms): responsabilidad de Plataforma o del proveedor gestionado (activo por defecto en los principales). Sin esto, una copia de etcd es una copia de todos los secretos. - RBAC de lectura (apartado 7): solo los
ServiceAccountde los pods que los montan y unas pocas personas de Plataforma puedengetsecretos; los desarrolladores de Pedidos, no. Y el manifiesto del Secret nunca está entechcorp/plataforma(GitOps es público dentro de la empresa). - Fuente de verdad externa con External Secrets Operator (ESO): el secreto vive en un gestor (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager...), y ESO crea y actualiza el
Secretde Kubernetes a partir de él. Así git solo contiene la referencia:
# k8s/servicio-pedidos/base/externalsecret-db.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: pedidos-db
namespace: techcorp
spec:
refreshInterval: 1h # ESO relee el gestor cada hora: si rotó, actualiza el Secret
secretStoreRef: { name: techcorp-vault, kind: ClusterSecretStore } # cómo hablar con el gestor (Vault con auth de Kubernetes)
target:
name: pedidos-db # el Secret que espera el Deployment de 05-02: nada cambia en él
creationPolicy: Owner
data:
- secretKey: PEDIDOS_DB_URL # clave en el Secret de Kubernetes
remoteRef: { key: techcorp/prod/pedidos/db, property: url } # ruta en el gestorkubectl get externalsecret pedidos-db -n techcorp # STATUS SecretSynced, READY True
kubectl get secret pedidos-db -n techcorp # creado por ESO; el manifiesto en git no contiene el valorRotación de credenciales. Una contraseña de base de datos vive años porque rotarla da miedo; con esta arquitectura, el procedimiento es rutinario (y se ejecuta al menos semestralmente y siempre tras una fuga, 07-03):
| Paso | pedidos-db (PostgreSQL) |
pedidos-rabbitmq |
|---|---|---|
| 1 | Crear la nueva contraseña de svc_pedidos en el gestor (techcorp/prod/pedidos/db); PostgreSQL permite tener dos válidas creando un usuario svc_pedidos_b o cambiando en ventana |
rabbitmqctl change_password pedidos <nueva> (las conexiones abiertas siguen; las nuevas usan la nueva) |
| 2 | ESO actualiza el Secret en ≤ 1 h (o kubectl annotate externalsecret pedidos-db force-sync=$(date +%s)) |
Igual |
| 3 | Los pods no releen variables de entorno: kubectl rollout restart deploy/servicio-pedidos (rolling, sin pérdida, 05-04); con secretos como ficheros (apartado 5) y un config.js que los relea, no haría falta reinicio |
Igual; el reconectar de amqplib (06-03) usa la nueva URL tras el reinicio |
| 4 | Revocar la contraseña antigua; comprobar en logs que no hay password authentication failed |
rabbitmqctl no tiene "doble contraseña": la ventana es el rolling restart |
Un Reloader (Stakater) o la anotación de Argo CD pueden automatizar el paso 3 cuando cambia el Secret.
- Secretos en CI: GitHub Environments y OIDC
En 05-03 el pipeline tenía GITHUB_TOKEN, PACT_BROKER_TOKEN, PLATAFORMA_TOKEN y KUBECONFIG_STAGING, y quedó pendiente la mención a OIDC. Lo que se cierra aquí:
- Environments de GitHub (
staging,production): secretos por entorno, required reviewers para producción, ydeployment branchespara que solomainpueda usarlos. Un workflow de una rama de funcionalidad no veKUBECONFIG_STAGING. - OIDC en vez de tokens de larga duración: con
permissions: id-token: write, el workflow obtiene un JWT firmado por GitHub que dice "soy el workflow X del repo Y en la rama Z", y la nube o el registro lo canjean por credenciales de minutos. Es el mismo client credentials de 07-01, con GitHub como emisor. Para el registro (ghcr.io) ya lo haceGITHUB_TOKEN; para un clúster gestionado en la nube:
# .github/workflows/servicio-node-cd.yml (fragmento: acceso a AWS/EKS sin access keys)
permissions: { id-token: write, contents: read }
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-techcorp-staging-deploy # rol con permiso SOLO sobre el clúster de staging
aws-region: eu-west-1
- run: aws eks update-kubeconfig --name techcorp-staging && kubectl rollout status deploy/servicio-pedidos -n techcorpCon esto KUBECONFIG_STAGING desaparece como secreto (la confianza es "este repo, esta rama, este workflow", configurada en la política del rol de la nube), y en producción no hay credencial ninguna: Argo CD hace pull (05-03). PLATAFORMA_TOKEN (para abrir el PR de promoción) se sustituye por una GitHub App con permisos acotados al repositorio plataforma. PACT_BROKER_TOKEN se mantiene como secreto de entorno, rotado por calendario.
Ficheros frente a variables de entorno. Las variables de entorno se ven en kubectl describe pod (no los valores de secretRef, pero sí de env: value), en /proc/<pid>/environ y las heredan los procesos hijos; los ficheros montados desde un Secret (volumes: - secret: { secretName: pedidos-db } + PEDIDOS_DB_URL_FILE=/etc/secretos/PEDIDOS_DB_URL) solo los lee quien tiene el UID/fsGroup adecuado, y Kubernetes los actualiza en caliente cuando el Secret cambia. La convención <VARIABLE>_FILE de 04-03 ya está implementada: TechCorp migra a ficheros los secretos de Pagos primero (auditoría) y el resto conforme se toque cada servicio.
- Red:
NetworkPolicy con deny-all por defecto
NetworkPolicy con deny-all por defectoSin NetworkPolicy, cualquier pod del clúster puede abrir una conexión con cualquier otro: un pod comprometido en techcorp llega a servicio-inventario:3006 con X-Usuario-Roles: admin (07-01) o a postgres:5432 para probar contraseñas. Las NetworkPolicy son firewall por etiquetas; requieren un CNI que las aplique (Calico, Cilium; los gestionados suelen traerlo). Estrategia: denegar todo en el namespace y abrir explícitamente.
# k8s/red/00-deny-all.yaml — nadie entra ni sale, salvo lo que se permita después
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: deny-all, namespace: techcorp }
spec:
podSelector: {} # todos los pods del namespace
policyTypes: [Ingress, Egress]
---
# k8s/red/01-permitir-dns.yaml — todos necesitan resolver nombres (03-05); sin esto, nada funciona
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: permitir-dns, namespace: techcorp }
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }, podSelector: { matchLabels: { k8s-app: kube-dns } } }]
ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
---
# k8s/red/gateway.yaml — el gateway es el ÚNICO que recibe del ingress controller, y solo habla con los servicios públicos
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: gateway, namespace: techcorp }
spec:
podSelector: { matchLabels: { app: gateway } }
policyTypes: [Ingress, Egress]
ingress:
- from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
ports: [{ port: 8080 }]
egress:
- to: [{ podSelector: { matchLabels: { app: servicio-catalogo } } }]
ports: [{ port: 3001 }]
- to: [{ podSelector: { matchLabels: { app: servicio-pedidos } } }]
ports: [{ port: 3002 }]
- to: [{ podSelector: { matchLabels: { app: servicio-clientes } } }]
ports: [{ port: 3004 }]
- to: [{ podSelector: { matchLabels: { app: bff-movil } } }]
ports: [{ port: 3010 }]
- to: [{ podSelector: { matchLabels: { app: keycloak } } }] # JWKS (07-01)
ports: [{ port: 8080 }]
---
# k8s/red/servicio-pedidos.yaml — recibe del gateway (y de Prometheus); habla con Catálogo, Clientes, RabbitMQ, PostgreSQL y Keycloak
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servicio-pedidos, namespace: techcorp }
spec:
podSelector: { matchLabels: { app: servicio-pedidos } }
policyTypes: [Ingress, Egress]
ingress:
- from: [{ podSelector: { matchLabels: { app: gateway } } }]
ports: [{ port: 3002 }]
- from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }] # /metrics (06-01)
ports: [{ port: 3002 }]
egress:
- to: [{ podSelector: { matchLabels: { app: servicio-catalogo } } }]
ports: [{ port: 3001 }]
- to: [{ podSelector: { matchLabels: { app: servicio-clientes } } }]
ports: [{ port: 3004 }]
- to: [{ podSelector: { matchLabels: { app: rabbitmq } } }]
ports: [{ port: 5671 }] # amqps (07-02)
- to: [{ podSelector: { matchLabels: { app: postgres } } }]
ports: [{ port: 5432 }]
- to: [{ podSelector: { matchLabels: { app: keycloak } } }]
ports: [{ port: 8080 }] # token de servicio y JWKS
- to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: observability } } }] # OTLP a Jaeger (06-02)
ports: [{ port: 4318 }]
---
# k8s/red/servicio-inventario.yaml — cierra lo que 07-02 dejó: a Inventario SOLO le habla Pedidos
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servicio-inventario, namespace: techcorp }
spec:
podSelector: { matchLabels: { app: servicio-inventario } }
policyTypes: [Ingress]
ingress:
- from: [{ podSelector: { matchLabels: { app: servicio-pedidos } } }]
ports: [{ port: 3006 }]
- from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }]
ports: [{ port: 3006 }]Y servicio-pagos es el único con egress a Internet (la pasarela): ipBlock: { cidr: 0.0.0.0/0, except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] } en el puerto 443, o mejor, si el CNI lo soporta (Cilium), una política por FQDN (api.pasarela-pagos.example). El resto de servicios no salen del clúster: un servicio-catalogo comprometido no puede exfiltrar datos ni descargar herramientas.
kubectl apply -f k8s/red/
# Comprobar: desde un pod cualquiera, Inventario no responde; desde Pedidos, sí
kubectl run prueba --rm -it --image=curlimages/curl -n techcorp -- curl -m 3 http://servicio-inventario:3006/health/live # timeout
kubectl exec deploy/servicio-pedidos -n techcorp -- wget -qO- http://servicio-inventario:3006/health/live # {"estado":"ok"}flowchart LR
IN[ingress-nginx] -->|8080| GW[gateway]
GW -->|3001| CA[servicio-catalogo]
GW -->|3002| PE[servicio-pedidos]
GW -->|3004| CL[servicio-clientes]
PE -->|3001| CA
PE -->|3004| CL
PE -->|3006| INV[servicio-inventario]
PE -->|5671 amqps| MQ[(rabbitmq)]
PE -->|5432| PG[(postgres)]
PA[servicio-pagos] -->|443| EXT((pasarela externa))
PR[prometheus] -.->|/metrics| PE & INV & CA & CL
X[cualquier otro pod] -. bloqueado .-x INV
Con las políticas, la advertencia de 07-01 sobre X-Usuario-* pasa de "cualquier pod" a "solo el gateway y Prometheus pueden alcanzar Pedidos" —y mTLS con mesh (07-02) añadirá, cuando llegue, identidad criptográfica además de red. Las políticas viven en techcorp/plataforma/k8s/red/ y las revisa Plataforma; cada servicio nuevo llega con la suya en la plantilla.
- RBAC de Kubernetes:
ServiceAccount por servicio y roles mínimos
ServiceAccount por servicio y roles mínimosTodo pod corre con un ServiceAccount (SA); por defecto, default, con un token montado en /var/run/secrets/kubernetes.io/serviceaccount que permite hablar con el API server. servicio-pedidos no necesita hablar con el API server, así que: SA propio (identidad clara para NetworkPolicy en algunos CNI, para mTLS con mesh en 07-02, y para imagePullSecrets) y sin token montado:
# k8s/servicio-pedidos/base/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata: { name: servicio-pedidos, namespace: techcorp }
automountServiceAccountToken: false # y en el Deployment (apartado 3) también; si un día necesita la API, un Role mínimo y token proyectado con caducidad
imagePullSecrets: [{ name: ghcr-techcorp }]Quien sí habla con el API server, con lo mínimo:
| Identidad | Necesita | Rol |
|---|---|---|
| Workflow de CD en staging (vía OIDC, apartado 5) | apply de Deployment/Service/ConfigMap/Job y rollout status en techcorp |
Role desplegador en techcorp (get/list/watch/create/update/patch sobre deployments, services, configmaps, jobs, pods solo lectura); RoleBinding al grupo/usuario federado. Nunca cluster-admin |
| Argo CD (producción) | Aplicar los manifiestos de overlays/prod |
Argo CD trae su SA; se le da un Role por namespace gestionado (techcorp, monitoring), no el ClusterRole admin por defecto de la instalación rápida |
| Prometheus | Descubrir pods y endpoints | ClusterRole de solo lectura (get/list/watch de pods, services, endpoints) |
| External Secrets Operator | Crear/actualizar Secret |
Role en cada namespace con secrets (create/update/get), no cluster-admin |
| Personas | Desarrollador de Pedidos: leer pods/logs y hacer port-forward en techcorp (view + pods/portforward); Plataforma: admin en techcorp; nadie usa cluster-admin a diario (cuenta de emergencia auditada) |
RoleBinding a grupos del proveedor de identidad (Keycloak/SSO también aquí) |
# k8s/rbac/desplegador.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: desplegador, namespace: techcorp }
rules:
- { apiGroups: ["apps"], resources: ["deployments"], verbs: ["get", "list", "watch", "create", "update", "patch"] }
- { apiGroups: [""], resources: ["services", "configmaps"], verbs: ["get", "list", "create", "update", "patch"] }
- { apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch", "create", "delete"] }
- { apiGroups: [""], resources: ["pods", "pods/log"], verbs: ["get", "list", "watch"] }
# ni "secrets" ni "delete deployments": los secretos los pone ESO; borrar es manual y auditadokubectl auth can-i list secrets -n techcorp --as=system:serviceaccount:techcorp:servicio-pedidos # no
kubectl auth can-i create deployments -n techcorp --as=desplegador-staging # yes
kubectl auth can-i '*' '*' --as=desplegador-staging # no
- Auditoría y detección
- Audit log del API server: registra quién hizo qué contra la API (
kubectl get secret pedidos-dbpor parte de una persona, uncreatedeRole, un pod que no cumple PSA). Se configura con unaPolicyde auditoría (nivelMetadatapara todo,RequestResponseparasecrets,rolesyrolebindings) y se envía a Loki (06-01) con retención larga, separada de los logs de aplicación (07-03). En clústeres gestionados se activa desde el proveedor. - Detección en tiempo de ejecución: Falco (mención) observa syscalls en los nodos y alerta ante comportamientos anómalos —una shell abierta en un contenedor de producción, un proceso que lee
/etc/shadow, una conexión saliente desde un pod que no debería salir— y se integra con Alertmanager (06-05). ConreadOnlyRootFilesystem,drop ALLy NetworkPolicies, la mayoría de esos comportamientos ya fallan; Falco cuenta el intento. - Higiene:
kube-bench(CIS Benchmark) para la configuración del clúster ykubectl get events -A --field-selector reason=FailedCreatepara ver pods rechazados por PSA.
- Cadena de suministro: lockfile, SBOM y SLSA
La cadena de suministro es todo lo que hay entre el código de Luis y el pod en producción, y cada eslabón puede sustituirse por algo malicioso. Lo que TechCorp fija: package-lock.json y npm ci (05-01/07-03: exactamente lo que se probó es lo que se despliega); dependencias y acciones de CI fijadas por versión (actions/checkout@v4, mejor por SHA para las críticas) y la imagen base por digest (node:20-alpine@sha256:…, actualizada por Renovate); SBOM por imagen con Syft (syft ghcr.io/techcorp/servicio-pedidos:1.0.1 -o spdx-json > sbom.json) adjunto a la imagen (cosign attest) para responder en minutos "¿qué servicios llevan libxml2 2.9.x?"; firma cosign y verificación en admisión (apartado 2); y SLSA (Supply-chain Levels for Software Artifacts) como marco de referencia: TechCorp está en el nivel 2 (build en CI con procedencia firmada) y aspira al 3 (runners efímeros y aislados). Solo mención: lo esencial es que el pipeline de 05-03 sea el único camino a producción, y que produzca artefactos verificables.
- Lista de comprobación de plataforma para TechCorp
Estado al cierre del módulo 7, mantenido en techcorp/plataforma/SEGURIDAD.md:
| Área | Comprobación | Estado |
|---|---|---|
| Imagen | Base node:20-alpine por digest, multi-stage, USER node, sin devDependencies |
Hecho (05-01) |
| Trivy CRITICAL/HIGH bloqueante + escaneo nocturno de imágenes desplegadas | Hecho / Pendiente (nocturno) | |
| Firma cosign + verificación en admisión | Pendiente (fase 2) | |
| Pod | securityContext completo (runAsNonRoot, drop ALL, seccomp, RO rootfs, emptyDir /tmp), límites de recursos |
Hecho en Pedidos y Catálogo; pendiente en gateway, Notificaciones |
PSA restricted en techcorp (enforce) |
warn activo; enforce pendiente hasta migrar RabbitMQ/Keycloak a namespace propio |
|
| Secretos | Cifrado en etcd (proveedor), RBAC de lectura, sin manifiestos de Secret en git |
Hecho |
ESO desde el gestor externo para todos los Secret de aplicación; rotación semestral documentada |
Hecho pedidos-db, pedidos-rabbitmq, pedidos-oidc, pagos-pasarela; resto pendiente |
|
Secretos como ficheros _FILE |
Pagos en curso | |
| CI | Environments con revisores; OIDC para nube y registro; sin kubeconfig en secretos; GitHub App en vez de PLATAFORMA_TOKEN |
Hecho / Pendiente (GitHub App) |
| Red | Deny-all + DNS + políticas por servicio; solo Pagos con egress externo | Hecho en techcorp |
| RBAC | SA por servicio sin token; Role desplegador; Argo CD acotado; sin cluster-admin diario |
Hecho / Argo pendiente |
| Auditoría | Audit log a Loki con retención larga; Falco | Hecho / Pendiente |
| Cadena | Lockfile, versiones fijadas, SBOM con Syft, SLSA 2 | Hecho salvo SBOM (en curso) |
Errores Comunes y Consejos
- Aplicar
deny-allsin la política de DNS: todo deja de resolver y parece que "Kubernetes se ha roto". DNS primero, siempre. - Olvidar el scraping de Prometheus, Jaeger o Keycloak en las políticas: métricas y trazas desaparecen en silencio. Revisa cada
to/fromcon los diagramas de 06-01/06-02. readOnlyRootFilesystemsinemptyDiren/tmp: la aplicación falla al arrancar conEROFSen la primera escritura de una dependencia.- PSA
enforcede golpe en un namespace con RabbitMQ, Keycloak o el ingress controller: pods rechazados.warn/auditprimero, luegoenforce. Secreten el repositorio GitOps "porque está en base64": es texto plano. Referencia conExternalSecret, valor en el gestor.- Rotar la contraseña y no reiniciar los pods (variables de entorno): fallan al reconectar horas después, de madrugada. Rotación = cambio + sincronización +
rollout restart(o ficheros en caliente). cluster-adminpara el pipeline o Argo CD "de momento": el momento dura años.Rolepor namespace desde el primer día.- Consejo: cada restricción nueva se prueba con un game day (06-03): intenta hacer lo prohibido (
kubectl runconroot,curla Inventario desde otro pod,kubectl get secretcomo desarrollador) y comprueba que falla y que queda en el audit log.
Ejercicios
Ejercicio 1. Escribe la NetworkPolicy de servicio-notificaciones (puerto 3005): consume de RabbitMQ (amqps), envía correo a un proveedor externo por HTTPS, expone /metrics a Prometheus, y nadie del clúster debe poder llamar a su API salvo Prometheus. Indica el riesgo que introduce el egress externo y cómo acotarlo.
Ejercicio 2. Un desarrollador de Catálogo pide poder ejecutar kubectl exec en los pods de servicio-catalogo en staging "para depurar Mongo". Diseña el Role/RoleBinding mínimo, di qué no incluye y propón una alternativa mejor coherente con esta lección.
Ejercicio 3. Recorre el modelo de amenazas del apartado 1 para el escenario "un atacante consigue ejecutar código dentro del pod de servicio-catalogo gracias a una dependencia comprometida" y enumera, capa por capa, qué medidas de esta lección limitan el daño y qué podría hacer aún.
Soluciones
Solución 1.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: servicio-notificaciones, namespace: techcorp }
spec:
podSelector: { matchLabels: { app: servicio-notificaciones } }
policyTypes: [Ingress, Egress]
ingress:
- from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: monitoring } } }]
ports: [{ port: 3005 }]
egress:
- to: [{ podSelector: { matchLabels: { app: rabbitmq } } }]
ports: [{ port: 5671 }]
- to: [{ podSelector: { matchLabels: { app: keycloak } } }] # si valida tokens o pide el suyo
ports: [{ port: 8080 }]
- to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] } }]
ports: [{ port: 443 }](DNS lo cubre permitir-dns.) El riesgo: el egress a "cualquier IP pública en 443" permite exfiltrar datos (correos, nombres) si el pod se compromete. Acotarlo: política por FQDN del proveedor de correo con Cilium (toFQDNs: [{ matchName: api.correo.example }]), o un proxy de salida (egress gateway) con lista blanca de dominios que además da un único punto de auditoría; y la minimización de 07-03 (Notificaciones no persiste datos personales) reduce lo que hay para exfiltrar.
Solución 2. Role depurador-catalogo en techcorp con pods (get/list), pods/log (get), pods/exec (create) limitado a los pods de Catálogo — RBAC no filtra por etiqueta, así que se hace con resourceNames (frágil, los nombres cambian) o, mejor, dando el Role en un namespace techcorp-catalogo si se separa por equipo; RoleBinding al grupo equipo-experiencia-compra del SSO. No incluye secrets, exec en otros pods, ni nada de escritura. Alternativas mejores: (1) kubectl debug con un contenedor efímero (kubectl debug -it pod/servicio-catalogo-xxx --image=mongo:7 --target=servicio-catalogo) en vez de exec en el contenedor de la aplicación, que además funciona con distroless; (2) un port-forward a MongoDB (pods/portforward) y el cliente de Mongo en local con el usuario svc_catalogo_ro de solo lectura; (3) que lo que necesite verse esté en logs/métricas (06-01) para no entrar en producción nunca. Y todo exec/debug queda en el audit log (apartado 8).
Solución 3. Con código ejecutándose como el usuario node (uid 1000) en el pod de Catálogo: Contenedor: no es root, drop ALL y seccomp bloquean la mayoría de vías de escape; readOnlyRootFilesystem impide instalar herramientas (solo /tmp de 64 MB); resources.limits evitan tumbar el nodo. Secretos: ve las variables de entorno del proceso (MONGO_URL de Catálogo) —con ficheros _FILE seguiría viéndolas, es su propio secreto—; no puede leer otros Secret (SA sin token, y sin RBAC). Red: solo alcanza MongoDB de Catálogo, Keycloak y lo que su política permita; no llega a Pedidos, Inventario, PostgreSQL ni RabbitMQ, y no sale a Internet (sin egress externo): puede leer/modificar el catálogo (su daño real) pero no exfiltrarlo fácilmente ni pivotar. Identidad: sin cabeceras válidas ni token de servicio de otro (los X-Usuario-* no le sirven porque no llega a otros servicios; con mesh, tampoco tendría certificado). Detección: Falco alertaría de la shell o del proceso raro; el audit log no vería nada porque no toca la API. Qué podría hacer aún: cambiar precios o descripciones en MongoDB (mitigado por auditoría de 07-03 y el usuario svc_catalogo sin dropDatabase), servir respuestas falsas a Pedidos (mitigado por validación con zod del ACL en Pedidos y precios congelados en el pedido) y consumir CPU hasta el límite. Y la lección de fondo: la dependencia comprometida debió detectarla Trivy/npm audit/SBOM antes de llegar (apartados 2 y 9).
Conclusión
Con esta lección la plataforma deja de ser el eslabón débil bajo la identidad, el canal y el código. TechCorp tiene un modelo de amenazas por capas (4C); imágenes mínimas escaneadas con Trivy (CRITICAL/HIGH bloqueante) y referenciadas por digest, con cosign y verificación en admisión como siguiente paso; pods con securityContext completo (runAsNonRoot, drop ALL, seccompProfile: RuntimeDefault, readOnlyRootFilesystem con emptyDir en /tmp) y Pod Security Admission restricted en techcorp; secretos cifrados en etcd, restringidos por RBAC y provistos por External Secrets Operator (ExternalSecret pedidos-db desde el gestor externo) con un procedimiento de rotación y rollout restart; CI con Environments y OIDC en lugar de kubeconfig y tokens de larga duración, y secretos como ficheros _FILE donde más importa; NetworkPolicy deny-all con DNS, gateway como única entrada desde el Ingress, Pedidos hacia Catálogo/Clientes/RabbitMQ/PostgreSQL/Keycloak, Inventario solo desde Pedidos —cerrando lo que 07-02 remitió aquí— y solo Pagos con salida a Internet; ServiceAccount por servicio sin token, Role desplegador y Argo CD acotados, sin cluster-admin; audit log del API server y Falco como detección; lockfile, versiones fijadas, SBOM y SLSA como cadena de suministro; y una lista de comprobación con lo hecho y lo pendiente. El módulo 7 termina así donde empezó el 6: con un sistema observable, resiliente, escalable, operable y ahora también seguro por capas —autenticación y autorización con Keycloak y JWT, canal cifrado y firmado, código validado y auditado, plataforma endurecida—, y con una lista honesta de lo que queda para la fase 2 (mTLS con mesh, cosign en admisión, PSA enforce, ESO para todos los servicios). El módulo 8 recoge todo el recorrido: cómo se ejecutó la migración del monolito paso a paso, la implementación completa de los servicios, su despliegue y operación de extremo a extremo, y las lecciones aprendidas por Marta, Luis y los cuatro equipos de TechCorp.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
