systemd
La unidad
Sección titulada «La unidad»[Unit]Description=ghchronicle, GitHub metrics collectorAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=ghchronicleGroup=ghchronicleEnvironmentFile=/etc/ghchronicle/ghchronicle.envExecStart=/usr/local/bin/ghchronicle -config /etc/ghchronicle/config.yamlRestart=alwaysRestartSec=30s
# This is the only process on the host holding a GitHub token, so it gets# nothing it does not need.NoNewPrivileges=truePrivateTmp=truePrivateDevices=trueProtectSystem=strictProtectHome=trueProtectKernelTunables=trueProtectKernelModules=trueProtectControlGroups=trueProtectClock=trueProtectHostname=trueProtectProc=invisibleRestrictNamespaces=trueRestrictRealtime=trueRestrictSUIDSGID=trueLockPersonality=trueMemoryDenyWriteExecute=trueSystemCallArchitectures=nativeSystemCallFilter=@system-serviceCapabilityBoundingSet=AmbientCapabilities=RestrictAddressFamilies=AF_INET AF_INET6
StateDirectory=ghchronicleReadWritePaths=/var/lib/ghchronicle
[Install]WantedBy=multi-user.targetPor qué está blindada
Sección titulada «Por qué está blindada»El modelo de amenaza es corto y es toda la justificación: este es muy probablemente el único proceso de la máquina que guarda un token de GitHub con acceso de lectura a todos los repositorios de una cuenta. Un token es una credencial al portador. Cualquier cosa que pueda leer la memoria de este proceso o su fichero de entorno tiene la cuenta.
Así que la unidad le da al proceso exactamente lo que necesita, que resulta ser casi nada: un socket TCP saliente y un directorio escribible.
| Directiva | Qué quita |
|---|---|
CapabilityBoundingSet=, AmbientCapabilities= | Todas las capacidades de Linux. No abre puertos privilegiados ni posee dispositivos |
No | Cualquier vía para ganar privilegios mediante exec, incluidos los binarios setuid |
Protect | El acceso de escritura a todo el sistema de ficheros, salvo ReadWritePaths |
Protect | Todos los directorios personales, que es donde suelen vivir las credenciales interesantes de una máquina |
PrivateTmp=true, PrivateDevices=true | Los ficheros temporales compartidos y los nodos de dispositivo físicos |
Protect | La capacidad de ver los procesos de otros usuarios en /proc, así que no puede leer la línea de órdenes de otro proceso |
Restrict | Los sockets Unix y netlink. Habla HTTPS y nada más |
MemoryDenyWriteExecute=true, LockPersonality=true | Las primitivas habituales de shellcode |
System | Toda llamada al sistema fuera del conjunto ordinario de servicio, incluidas las de módulos y ajuste del núcleo |
ProtectKernelTunables, ProtectKernelModules, ProtectControlGroups, ProtectClock, ProtectHostname, RestrictNamespaces, RestrictRealtime, RestrictSUIDSGID | Todas las vías que quedan para cambiar la máquina desde dentro del servicio |
StateDirectory=ghchronicle hace que systemd cree /var/lib/ghchronicle con el
dueño correcto al arrancar, así que el fichero de estado tiene dónde vivir sin
un mkdir manual y un chown que alguien olvidará tras una reinstalación.
Ahí viven cuatro ficheros, no uno. Junto a state.json la pasada guarda su
registro de escrituras, state-written.bin por omisión, que es lo que evita
volver a escribir un punto que no ha cambiado, y su caché, state-cache.bin,
que es lo que permite a un reinicio preguntar a GitHub solo por lo que cambió;
y el servicio mantiene state-lock mientras corre, que es como -migrate -yes
sabe que no debe cambiar los almacenes por debajo de él. Un relleno, o la
relectura de una migración,
que se detiene a medias deja también ahí su punto de control. ReadWritePaths
cubre el directorio, así que todos están ya permitidos. Si
pones el fichero de estado o el registro en otro sitio, esa ruta hay que
añadirla aquí, y la caché sigue al fichero de estado vaya donde vaya. Perder el
registro cuesta una pasada de reescritura:
solo se escribe lo que ha cambiado;
perder la caché, una pasada a precio completo:
la caché que hay a su lado.
Instalarla
Sección titulada «Instalarla»-
Crea el usuario y los directorios.
Ventana de terminal sudo useradd --system --no-create-home --shell /usr/sbin/nologin ghchroniclesudo mkdir -p /etc/ghchronicle -
Pon la configuración en su sitio.
Directorio/etc/ghchronicle/
- config.yaml legible por todos, sin secretos dentro
- ghchronicle.env modo 600, los tokens
Directorio/var/lib/ghchronicle/
- state.json lo crea el servicio
- state-written.bin el registro de escrituras, al lado
- state-cache.bin la caché, también al lado
- state-lock lo mantiene el servicio mientras corre
-
Escribe el fichero de entorno, y nada más en él.
/etc/ghchronicle/ghchronicle.env GITHUB_TOKEN=github_pat_...INFLUX_TOKEN=...Ventana de terminal sudo chmod 600 /etc/ghchronicle/ghchronicle.envTodo en
config.yamllos lee mediante${VAR}, que es lo que permite que la configuración sea legible por todos y esté versionada mientras los secretos no lo están. -
Arráncalo.
Ventana de terminal sudo systemctl daemon-reloadsudo systemctl enable --now ghchroniclesudo systemctl status ghchronicleLa primera pasada corre todas las familias, porque aún no ha corrido ninguna, salvo que el servicio arranca como mucho una familia de seis horas o más por pasada:
traffic,stats,forksy el resto de las once llegan al almacén a lo largo de las dos primeras horas y media, y solo esta vez. Ver las familias lentas se turnan.
Qué vigilar
Sección titulada «Qué vigilar»El log dice qué se escribió y dónde.
level=INFO msg=written sink=influxdb family=repo points=934 unchanged=1955level=INFO msg="rate budget" bucket=core remaining=4477 limit=5000Qué dice el log un día bueno tiene todas las líneas de rutina.
Tres avisos merecen una alerta:
rate limit reserve reachedsignifica que se saltó una familia para proteger el presupuesto. Una vez está bien; en cada pasada significa que las cadencias son demasiado rápidas para el número de repositorios.family failed everywhere, not marking it as runsignifica que todos los repositorios fallaron en una familia, así que se reintentará en vez de darla por hecha.migration pending, en cada arranque tras una actualización, significa que un almacén aún guarda filas con una forma que esta versión ya no escribe, y que el arranque las dejó: mira tras una actualización.
journalctl -u ghchronicle -fjournalctl -u ghchronicle -p warning --since todayTras una actualización
Sección titulada «Tras una actualización»Sustituye el binario y reinicia el servicio. Con el valor por omisión,
migrate: auto, el arranque comprueba
cada almacén frente a los cambios que trae la nueva versión y, antes de su
primera pasada, aplica por su cuenta cada uno que no pierde nada, y lo dice en
WARN. Un cambio que deja es un WARN en cada arranque, con el almacén, el
motivo y las dos órdenes: lo que hace un
arranque.
Ejecútalas con el propio usuario del servicio y con su fichero de entorno, con
el servicio parado, porque -migrate -yes se niega a correr a su lado:
sudo systemctl stop ghchroniclesudo systemd-run --uid=ghchronicle --gid=ghchronicle --pipe --wait --collect \ --property=EnvironmentFile=/etc/ghchronicle/ghchronicle.env \ /usr/local/bin/ghchronicle -config /etc/ghchronicle/config.yaml -migratesudo systemd-run --uid=ghchronicle --gid=ghchronicle --pipe --wait --collect \ --property=EnvironmentFile=/etc/ghchronicle/ghchronicle.env \ /usr/local/bin/ghchronicle -config /etc/ghchronicle/config.yaml -migrate -yessudo systemctl start ghchronicleLa primera imprime el plan y no cambia nada; la segunda lo aplica y vuelve a
leer lo que despejó. systemd-run lee el fichero de entorno como root, igual que
la unidad, así que puede seguir en modo 600, y ejecuta el binario como
ghchronicle: medido en systemd 257, una orden lanzada así vio el token de un
fichero que solo root podía leer y corrió como el usuario indicado. Si la lanza
root, -migrate -yes guarda como de root, en modo 600, los ficheros que
reescribe junto al de estado, state.json entre ellos, y el servicio, que corre
como ghchronicle, se detiene entonces al arrancar y nombra el fichero en vez
de empezar con uno nuevo, que olvidaría una relectura aún debida: devuélveselo
con chown ghchronicle:ghchronicle.
Con -once lanzado desde cron en vez de un servicio, comenta la línea mientras
duran las dos órdenes: un -once no tiene ningún bloqueo mientras hace su
pasada, así que nada le impide correr a la vez que -migrate -yes.
cron en vez de un servicio
Sección titulada «cron en vez de un servicio»-once ejecuta una sola pasada y termina, que es todo lo que necesita un
planificador.
0 * * * * /usr/local/bin/ghchronicle -config /etc/ghchronicle/config.yaml -onceDeja el fichero de estado en una ruta persistente también en este modo. Sin él
cada ejecución recoge todas las familias, diga lo que diga su cadencia, y
vuelve a recorrer la lista de estrellas, el historial entero de estrellas y las
pull requests en coautoría; el fichero de caché que hay a su lado es lo que
deja a una ejecución preguntar a GitHub solo por lo que cambió. Ten en cuenta
que un cron horario da a cada familia una cadencia horaria como mucho, así que
las cinco que corren cada quince minutos, actions, events, notifs,
activity y ratelimit, corren cuatro veces menos: el tiempo en cola de una
ejecución de workflow se lee cuando ya ha terminado, y un hilo de
notificaciones que se movió dos veces dentro de la hora muestra solo su segundo
movimiento.