---
title: "El sync terminó bien y el pod montó la raíz del nodo"
description: "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."
canonical: "https://vtsecurity.com.ar/articulo/gitops-desplego-lo-que-decia-git"
author: "Valentín Torassa"
lang: "es-AR"
datePublished: "2026-10-23T12:00:04.379Z"
dateModified: "2026-10-23T12:00:04.379Z"
---

# 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.

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.

*Laboratorio local con kind y kubectl apply; no configuré checks obligatorios para el merge.*

## De `main` al cluster

GitOps usa Git como referencia para el estado del cluster. Por ejemplo, [Argo CD puede sincronizar automáticamente](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/) 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:

```diff
-            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:

1. `git verify-commit HEAD` falló: el commit no estaba firmado.
2. `kyverno apply` encontró dos violaciones: `hostPath` y `privileged`.
3. La cuenta del reconciler tenía un `Role` limitado al namespace `prod`.
4. 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](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) 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](https://kubernetes.io/docs/concepts/security/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.

*Resultados del lab local: la firma falló, Kyverno detectó dos violaciones y Pod Security rechazó los Pods.*

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](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) 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.

*La cuenta amplia aplicó el manifest. La acotada recibió Forbidden en un dry-run.*

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](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/#automatic-self-healing); 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.

*El script volvió a sincronizar de forma explícita. No simulé self-heal automático.*

## Qué revisaría en una instalación real

1. **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.
2. **Si el check realmente bloquea:** probá un manifest con `privileged: true` o `hostPath` en un entorno de prueba. Confirmá que el job falla y que el merge no se habilita. No alcanza con ver `fail: 2` en el log.
3. **Qué puede aplicar el controller:** corré `kubectl auth can-i` con su identidad. Revisá en especial `secrets`, `roles`, `rolebindings` y recursos de alcance cluster.
4. **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.
5. **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.
