¿Protección de ramas desactivada? Audita los rulesets
Investiga la manipulación de la protección de ramas y rulesets de GitHub: protected_branch.destroy, cambios de ruleset, overrides, entornos y lo que entró.
En resumen. La protección de ramas y los rulesets son lo que impide que una sola credencial robada reescriba main. Los atacantes los eliminan (protected_branch.destroy, repository_ruleset.destroy), los debilitan (protected_branch.update_*, repository_ruleset.update) o los eluden (protected_branch.policy_override). Hacen lo mismo con los entornos de despliegue (environment.remove_protection_rule). El registro de auditoría te dice quién cambió qué regla y cuándo. El historial de Git y los eventos Git te dicen qué entró mientras estaba desactivada. Restaura la regla y revisa después cada push, force-push y despliegue de esa ventana.
La revisión de código, las comprobaciones de estado obligatorias y los commits firmados son controles de la cadena de suministro. Solo funcionan mientras se aplican. Un atacante con permisos de administración o con el token de un propietario puede desactivarlos durante los minutos que necesite. Algunos los vuelven a activar después para ocultar el hueco. El registro de auditoría conserva los dos eventos.
Qué se puede manipular
| Control | Eliminado / borrado | Debilitado | Eludido |
|---|---|---|---|
| Protección de ramas clásica | protected_branch.destroy | protected_branch.update_* (número de revisiones, comprobaciones de estado, force-pushes, borrados, aplicación a administradores…) | protected_branch.policy_override |
| Rulesets de repositorio | repository_ruleset.destroy | repository_ruleset.update | bypass actors configurados en el ruleset |
| Entornos de despliegue | environment.delete | environment.remove_protection_rule, environment.update_protection_rule | — |
La documentación de GitHub sobre ramas protegidas y rulesets describe cada ajuste. La entrada del glosario sobre rulesets de repositorio explica cómo se combinan los rulesets con la protección clásica.
Cómo lo señala el analizador
| Regla | Acciones | Gravedad |
|---|---|---|
branch-protection-removed | protected_branch.destroy, repository_ruleset.destroy | alta |
branch-protection-override | protected_branch.policy_override | media |
environment-protection-removed | environment.remove_protection_rule, environment.delete | alta |
Los hallazgos se agrupan por repositorio. Las acciones de debilitamiento (protected_branch.update_*, repository_ruleset.update) no son reglas, porque forman parte de la administración normal de un repositorio. Aparecen en la cronología porque allí se conservan todas las acciones protected_branch. y repository_ruleset.. Cuando salte un hallazgo, abre la cronología de ese repositorio y lee las actualizaciones que lo rodean.
Cómo leer una eliminación
Un evento protected_branch.destroy incluye el repositorio, el patrón de rama (name) y los ajustes que tenía la regla en ese momento, como required_approving_review_count y admin_enforced. Eso te dice qué se perdió. Después, responde a cuatro preguntas:
- ¿Quién y con qué? Actor, IP, user agent y
hashed_token. Una sesión de navegador desde la oficina del propietario no es lo mismo quecurl/8.5.0con un token desde un VPS. - ¿Se restauró, y quién lo hizo? Un
protected_branch.createpara la misma rama horas después, hecho por otro actor, suele significar que alguien se dio cuenta. Que el mismo actor la vuelva a crear minutos después apunta a un intento de ocultación. - ¿Qué se subió entretanto? Filtra los eventos Git por
git.pushen ese repositorio dentro de la ventana y compáralos con el historial de commits de la rama. Los eventos Git excluyen los pushes hechos desde la interfaz web o la API, así que revisa también la vista de actividad del repositorio. - ¿Se desplegó algo? Las ejecuciones de workflows en la rama por defecto durante la ventana (
workflows.created_workflow_runconhead_branch: main) pueden haber puesto el cambio en producción.
Los overrides son otra cosa
protected_branch.policy_override significa que un administrador del repositorio hizo push o merge mientras la regla seguía activa, usando su privilegio para saltársela. A menudo es legítimo: hotfixes o responsables de release con una excepción documentada. Por eso tiene gravedad media. Se vuelve interesante cuando la cuenta de administrador también tiene hallazgos de token o de ubicación, o cuando los overrides se concentran en rutas sensibles como .github/workflows/.
Entornos: los secretos detrás de la barrera
Las reglas de protección de entornos (revisores obligatorios, temporizadores, políticas de ramas de despliegue) suelen ser lo único que separa un workflow en una rama cualquiera de las credenciales cloud de producción guardadas como secretos de entorno. Eliminar una regla, o borrar el entorno y volver a crearlo, deja esos secretos al alcance de cualquier ejecución de workflow que haga referencia al entorno. Tras un hallazgo environment-protection-removed, lista las ejecuciones que usaron ese entorno durante la ventana y considera expuestos sus secretos. El artículo sobre secretos de Actions explica cómo acotarlo.
Force-pushes y reescrituras del historial
Las reglas suelen bloquear los force-pushes. Con la protección desactivada, un atacante puede reescribir el historial para introducir un commit malicioso o para borrar el rastro de uno. El registro de auditoría no guarda el contenido de los commits, pero todavía puedes trabajar con:
- Los eventos
git.push(si tienes los eventos Git) para la cronología y la credencial utilizada. protected_branch.update_allow_force_pushes_enforcement_level, si el atacante solo relajó ese ajuste.- La vista de actividad del repositorio y tus mirrors o cachés de CI. Puede que aún conserven los commits anteriores a la reescritura.
Medir el hueco a mano
Si quieres contrastar la lectura del analizador, o solo tienes la exportación en bruto, la consulta es corta. Extrae todos los eventos de protección y de rulesets del repositorio, en orden cronológico, con el actor y la IP:
jq -r '.[] | select(.repo=="ORG/REPO")
| select(.action|test("^(protected_branch|repository_ruleset|environment)\\."))
| [(.["@timestamp"]/1000|todate), .action, .actor, (.actor_ip // "-"), (.name // "-")]
| @tsv' audit-log.json | sort
Lee la salida por parejas. Cada destroy o update que debilita una regla debería ir seguido de un create o update que la restaura. El tiempo entre ambos es tu ventana de exposición. Anótala en UTC, porque todo lo demás del acotamiento (pushes, ejecuciones de workflows, despliegues) se filtra por esa ventana. Si no hay una restauración correspondiente, la ventana sigue abierta. Corrígelo antes de continuar con la investigación. Comprueba también si el debilitamiento y la restauración salieron de la misma cuenta. Cuando una persona desactiva la protección y la vuelve a activar alrededor de un único push, tienes que hablar con esa persona, sea cual sea el motivo al final.
Lista de remediación
- Vuelve a crear la protección de ramas y los rulesets a partir de una definición fiable (mantén los rulesets como código, o expórtalos).
- Restaura las reglas de protección de los entornos y rota los secretos del entorno.
- Revisa cada commit, merge, force-push y despliegue hecho durante el hueco, y revierte o vuelve a desplegar desde un commit de confianza.
- Elimina los bypass actors inesperados de los rulesets y los administradores de repositorio inesperados.
- Para los controles más importantes, prefiere los rulesets de organización. Se gestionan a nivel de organización, no los administradores de cada repositorio.
Relacionado
- Toma de control de una organización de GitHub: propietarios, 2FA, SSO y lista de IP permitidas
- Fuga de secretos de GitHub Actions: investigar la exfiltración
- El caso práctico ficticio de northwind-labs: protección de rama eliminada a las 03:05 UTC y restaurada a las 07:48
- MITRE ATT&CK T1562 Impair Defenses