Un bot con límite de cero repos me hizo fork de todas formas
Read in English →Tengo mi propio Gitea en la casa. Ahí viven todos mis repos, y los runners de Actions que construyen y despliegan todo viven en el mismo clúster de Kubernetes. El 30 de septiembre estaba conectando un bot de Telegram, parakletos-bot, para que pudiera abrir pull requests de GitOps cuando yo no estoy frente al computador. Eso significaba darle a una cuenta bot acceso a Gitea, así que me senté a revisar qué puede hacer realmente una cuenta bot.
Yo pensaba que lo tenía bien amarrado. Usuario restringido. Privado. Sin admin. Límite de creación de repos en 0, para que no pueda crear nada. Permiso de escritura solo en el único repo que necesita.
Entonces me hice una pregunta sencilla. ¿Qué pasa si el bot hace fork de un repo?
Lo que “límite 0” significaba para mí
En mi cabeza, un fork es un repo nuevo bajo la cuenta del bot. El bot tiene un límite de 0 repos. Entonces el fork debería fallar. Listo.
Así no funciona Gitea por defecto. Hay una opción bajo [repository] que se llama ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT, y viene en true. El nombre lo dice todo cuando uno lo lee despacio: los forks se permiten aunque el usuario ya esté en su máximo. El límite de creación aplica a crear repos, y un fork no cuenta.
O sea que mi bot restringido con límite 0 podía hacer fork de cualquier repo que pudiera leer. Lo probé. Funcionó.
Por qué un fork es un problema
Un fork solo no hace daño. El problema es lo que viene montado encima.
Los workflows de Gitea Actions viven en .gitea/workflows/ dentro del repo. Un fork se lleva esa carpeta, y el dueño del fork la puede editar. Mis runners estaban registrados de forma global, lo que significa que cualquier repo de la instancia podía mandarles jobs. Y esos runners no son de juguete. Corren con un Docker-in-Docker compartido y en la red del host, porque eso es lo que toca para construir imágenes de contenedores en Kubernetes.
Junta las piezas. Una cuenta bot con límite 0 hace fork de un repo, edita un workflow en el fork, hace push, y ya tiene código arbitrario corriendo en un runner privilegiado dentro de mi clúster. Los labels del runner no detuvieron nada. Los labels deciden dónde corre un job. No son control de acceso.
Yo venía tratando “¿puede hacer push a mi repo?” como la frontera de seguridad. La frontera real era “¿puede meter un workflow en mi runner?”, y el camino del fork le dio la vuelta a la primera sin problema.
El arreglo
La parte del fork fue una línea en los values del Helm chart de Gitea:
gitea:
config:
repository:
ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT: false
Con eso, el intento de fork del bot devuelve un 409. Los usuarios sin límite, como mi propia cuenta, no se ven afectados.
La parte del runner me tomó un poco más. Borré los runners globales y registré uno con alcance a mi usuario. Ahora solo mis repos lo pueden usar, y un repo que no sea mío no tiene dónde aterrizar aunque un workflow llegara por ahí.
Y el bot terminó sin credenciales de Gitea. Un sidecar pequeño guarda el token y solo hace commits por la API de archivos, a una lista cerrada de rutas. El bot le pide al sidecar; el sidecar decide. Pero eso es otro post.
Con qué me quedo
Ojo: hay que leer el default, no el nombre. “Límite de creación de repos” sonaba como si cubriera los forks. No los cubría, y la opción que controla los forks estaba una sección más abajo con true por defecto.
Y en cualquier sistema de CI donde los workflows viven en el repo, tener escritura en un repo es ejecutar código en el runner, con los secretos de ese repo. Si un bot puede meter un workflow en un runner por cualquier camino, incluido un fork, el bot tiene el runner. Hay que acotar el runner. No confiar en los labels.
Todo esto me tomó una tarde en encontrarlo y unos diez minutos en arreglarlo. Lo caro fue asumir que el límite significaba lo que yo quería que significara.