El sync terminó bien y el pod montó la raíz del nodo
En un laboratorio de GitOps, un manifest privilegiado llegó a main y el reconciler lo aplicó. Qué detectaron la firma y Kyverno, qué bloqueó Kubernetes y qué revisar en una instalación real.
En este artículo
El commit decía hotfix: debug access. El diff quitaba las restricciones del contenedor, activaba privileged: true y montaba la raíz del nodo en /host. Llegó a main; el reconciler lo aplicó sin errores. En el cluster aparecieron pods privilegiados con ese montaje.
Eso pasó en el laboratorio local que armé para mi charla en DevSecOps Space de Ekoparty. El fallo estaba antes del sync: el manifest inseguro ya había entrado a Git.
Pods privilegiados en ejecución. Git tenía el manifest inseguro y Kubernetes lo ejecutó.
De main al cluster #
GitOps usa Git como referencia para el estado del cluster. Por ejemplo, Argo CD puede sincronizar automáticamente cuando ve diferencias entre los manifests del repo y el estado actual. Eso facilita seguir los cambios y volver a una versión anterior, pero también hace que la ruta hacia main sea parte de la seguridad del cluster.
Para probarlo usé un repo de manifests, un cluster kind y un script que ejecuta kubectl apply con la service account del reconciler. No hay Argo CD ni un PR real detrás: los controles de la rama se probaron con comandos locales. Así podía ver por separado qué se aceptaba en Git y qué podía aplicar esa cuenta.
En la primera cadena, el cambio entró directo a main sin firma ni revisión. El extracto del diff era este:
- allowPrivilegeEscalation: false
- runAsNonRoot: true
- seccompProfile: { type: RuntimeDefault }
- capabilities: { drop: [ALL] }
+ privileged: true
+ volumeMounts:
+ - { name: host, mountPath: /host }
+ volumes:
+ - name: host
+ hostPath: { path: / }La cuenta del reconciler tenía cluster-admin y el namespace no exigía un nivel de Pod Security. kubectl apply respondió deployment.apps/app created; al consultar los pods, el laboratorio registró PRIVILEGED true y HOSTPATH /. El sync fue correcto con respecto a Git; el resultado era inseguro.
Qué detectó la segunda cadena #
En la segunda cadena dejé el manifest en una rama y probé cuatro controles:
git verify-commit HEADfalló: el commit no estaba firmado.kyverno applyencontró dos violaciones:hostPathyprivileged.- La cuenta del reconciler tenía un
Rolelimitado al namespaceprod. - Ese namespace tenía Pod Security Admission en nivel
restricted.
El punto 2 merece cuidado: la salida del laboratorio dice fail: 2, pero el comando la pasa por grep y terminó con código 0. Encontrar una violación no equivale a impedir un merge. En un pipeline real, el check debe fallar y ser obligatorio para entrar a la rama protegida. Las reglas de protección de GitHub permiten exigir reviews, checks y commits firmados; el laboratorio no configuró esas reglas en un servidor Git.
El laboratorio también probó qué pasaría si el manifest llegara igual al API server. Kubernetes aceptó el objeto Deployment y mostró un warning, pero Pod Security Admission rechazó la creación de sus pods. La evidencia fue un evento FailedCreate con forbidden: violates PodSecurity "restricted:latest". Un Deployment creado no significa que la aplicación esté corriendo.
Dos violaciones. El check terminó en 0.
Kyverno reportó hostPath y privileged. El script filtró la salida con grep y terminó en 0: la detección no se convirtió en un check fallido.
$ kyverno apply policies/ --resource prod/deployment.yamlpass: 0, fail: 2, warn: 0 rc del script: 0
Son momentos distintos. Un check obligatorio puede frenar el merge. Pod Security decide si se crean los Pods cuando el manifest llega al API server. El Role define qué operaciones puede pedirle a Kubernetes la cuenta del reconciler.
Qué pudo hacer la cuenta de sync #
En otro escenario, un manifest creó una service account en kube-system y la vinculó a cluster-admin. La cuenta amplia lo aplicó y la nueva cuenta terminó pudiendo hacer * * en el cluster. En la cadena acotada, la prueba con la service account del reconciler devolvió Forbidden para serviceaccounts en kube-system y para clusterrolebindings a nivel cluster.
El Role del laboratorio permitía operar deployments, services y configmaps en prod. No permitía crear cuentas ni bindings de alcance cluster. RBAC de Kubernetes recomienda conceder solo los recursos y verbos necesarios; en una instalación real habría que revisar qué recursos administra el controller antes de recortar permisos.
Una creó un admin. La otra recibió Forbidden.
La nueva cuenta terminó con permisos * *.
La API rechazó la service account y el binding fuera de su alcance.
También probé un Secret con un valor ficticio en texto plano. La cadena amplia lo aplicó y git log -p -S todavía encontraba el valor en el historial. En la otra, la política lo marcó como fallo y el Role tampoco permitía tocar Secrets. Borrar el archivo en un commit posterior no borra el commit que ya lo contenía; el control útil es impedir que entre y gestionar los secretos fuera de ese manifest en claro.
Cuando cambia el cluster y cuando cambia Git #
Alguien cambió el cluster a mano: cinco réplicas y nginx:latest donde Git decía dos y una imagen fijada. El laboratorio mostró el desvío con kubectl diff; después ejecutó otra sincronización y volvió a dos réplicas con la imagen de Git. Esa vuelta fue un paso explícito del script. En Argo CD, los cambios hechos directamente en el cluster no disparan self-heal por defecto; hay que configurarlo si se espera esa conducta.
El último escenario fue diferente: un cambio firmado pasó las políticas, llegó a main y después resultó ser una mala decisión para la aplicación. La corrección fue un git revert firmado y una nueva sincronización. La firma confirmó el uso de la clave de prueba; no garantizaba que la decisión fuera buena. Las políticas tampoco pueden decidir si una versión sirve para todos tus usuarios.
Git decía 2 réplicas. El cluster tenía 5.
nginxinc/nginx-unprivileged:1.27-alpinenginx:latestAlguien cambia el cluster a mano. Git sigue diciendo otra cosa.
$ kubectl scale deploy/app --replicas=5
$ kubectl set image deploy/app web=nginx:latestdeployment.apps/app scaled deployment.apps/app image updated
La firma prueba el uso de la clave; no juzga si la versión conviene. Se revirtió en Git y se volvió a sincronizar.
- 01Versión buena
- 02Cambio firmado
- 03git revert firmado
En Argo CD, selfHeal para cambios directos en el cluster no viene activado por defecto.
Qué revisaría en una instalación real #
- Quién puede cambiar
main: PR obligatorio, revisión de la parte sensible y checks requeridos. Revisá quién tiene permiso de bypass. Una firma prueba posesión de una clave; la revisión evalúa el cambio. - Si el check realmente bloquea: probá un manifest con
privileged: trueohostPathen un entorno de prueba. Confirmá que el job falla y que el merge no se habilita. No alcanza con verfail: 2en el log. - Qué puede aplicar el controller: corré
kubectl auth can-icon su identidad. Revisá en especialsecrets,roles,rolebindingsy recursos de alcance cluster. - Qué acepta el cluster: comprobá los niveles de Pod Security de los namespaces y las excepciones. Las políticas del PR y la admisión cubren momentos distintos.
- Cómo detectás y corregís desvíos: mirá el estado en vivo, la configuración real de self-heal y el procedimiento para revertir desde Git.
Que el cluster coincida con Git no prueba que el cambio sea seguro. Para saber si ese estado es aceptable, miraría el merge, los permisos del reconciler y lo que Kubernetes terminó creando.
Lo próximo llega también por mail. Cada dos semanas, en Apuntes.
Comentarios
Pasan por moderación antes de aparecer.