Instalar un runner autohospedado de GitHub Actions
Configura un runner de GitHub Actions en Ubuntu 24.04 con usuario dedicado, checksum, config.sh y systemd, y revisa el riesgo de pull requests desde forks.
Qué hace un runner de GitHub Actions autohospedado
Un runner de GitHub Actions autohospedado es un programa que instala en su propio VPS. Solicita trabajos a GitHub y los ejecuta en su hardware. Se registra para un repositorio, se instala como servicio de systemd y vuelve a iniciarse después de cada reinicio. GitHub programa el trabajo. El servidor hace el trabajo.
La CI (integración continua) en un equipo propio resulta útil por dos motivos. Los minutos de compilación dejan de contabilizarse. Además, un trabajo puede acceder a recursos que solo están disponibles en su máquina, como una caché de compilación activa o una red privada. El coste es la seguridad. El runner ejecuta todo lo que indique el archivo de workflow, con el usuario que le haya asignado. Por diseño, un archivo de workflow permite la ejecución remota de código. En un repositorio privado, esto no supone un problema porque solo las personas de confianza pueden añadirlo. En un repositorio público, es un riesgo real. La sección sobre las solicitudes de incorporación de cambios desde forks explica el mecanismo.
Todo lo que sigue usa Ubuntu 24.04 y la versión 2.336.0 del runner, la versión actual en julio de 2026.
Qué necesitas antes de empezar
Empieza con un VPS que tenga una cuenta de administrador normal y sudo, en el estado que alcanzas en los primeros diez minutos en un VPS nuevo. No necesitas abrir ningún puerto de entrada. El runner abre una conexión HTTPS (protocolo seguro de transferencia de hipertexto) saliente a GitHub y la mantiene abierta mientras espera trabajo. Por tanto, GitHub nunca se conecta a tu servidor. El firewall puede permanecer cerrado al exterior y los trabajos seguirán llegando.
También necesitas permisos de administrador en el repositorio, porque el token de registro se muestra en la configuración del repositorio.
Crear un usuario dedicado para el runner
Nunca ejecutes el runner como root ni como tu propio usuario administrador. Cada trabajo hereda los permisos del usuario del runner, por lo que un flujo de trabajo que llama a sudo funciona si el usuario del runner puede usar sudo. Crea un usuario sin privilegios que no sea propietario de nada excepto de su propio directorio personal. Cuentas de usuario con privilegios mínimos en un VPS explica el patrón general. Este es el procedimiento específico.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l bloquea la contraseña, por lo que nadie puede iniciar sesión como gharunner con ella. El modo 700 en el directorio del runner es importante porque el runner almacena allí sus credenciales en texto sin cifrar, y un checkout puede contener código fuente privado.
Comprueba ambas propiedades antes de continuar:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S muestra una línea que empieza por gharunner L, donde L significa que la contraseña está bloqueada. sudo -l -U gharunner debe responder con is not allowed to run sudo. Si muestra una lista de comandos permitidos, la cuenta pertenece a un grupo de sudo y el aislamiento que acabas de crear se ha perdido.
Descargue el runner y compruebe el archivo tar
A partir de este punto, trabaje como el usuario runner.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"Ejecute primero uname -m si no está seguro de la arquitectura. x86_64 usa el archivo linux-x64 anterior. aarch64 usa actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Ahora compruebe lo que descargó. El SHA256 (algoritmo de hash seguro de 256 bits) siguiente corresponde al archivo tar de 2.336.0 x64. GitHub muestra el valor de la versión actual en la página de la versión y en la pantalla New self-hosted runner. El valor cambia con cada versión, así que cópielo de allí cuando instale otra.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cUna descarga correcta muestra una línea:
actions-runner-linux-x64-2.336.0.tar.gz: OKUn archivo truncado o modificado muestra el fallo y una advertencia:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchNo omita esta comprobación para que tar encuentre el problema después. Un archivo incompleto falla con gzip: stdin: unexpected end of file y tar: Unexpected EOF in archive. Esto indica que el archivo está dañado, pero no si quedó truncado o fue reemplazado.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsQué contiene el tarball y qué no contiene
Después de la extracción, el directorio contiene config.sh, run.sh, env.sh, safe_sleep.sh, bin/ y externals/. bin/ contiene los binarios del ejecutor y bin/installdependencies.sh. externals/ contiene el entorno de ejecución de Node incluido que ejecutan las acciones de JavaScript.
Todavía no existe svc.sh. La documentación de GitHub lo describe como el script «que se crea después de agregar correctamente el ejecutor», porque se escribe a partir de una plantilla con el repositorio y el nombre del ejecutor incorporados en el nombre del servicio. Por eso, sudo ./svc.sh install antes de ./config.sh falla con sudo: ./svc.sh: command not found. Registre primero el ejecutor y, después, instale el servicio.
Instalar las dependencias del runner
El runner es una aplicación .NET, por lo que necesita varias bibliotecas compartidas. Mantenga el shell del usuario del runner e instálelas con sudo, porque el script escribe en la base de datos de paquetes del sistema.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shEn Ubuntu 24.04, esto instala libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 y libicu74. El script prueba varios nombres de versión para cada biblioteca y conserva el que proporciona su release. Por eso, el mismo script funciona en versiones anteriores de Ubuntu y en Debian.
Si omite este paso, ./config.sh se detiene antes de hacer nada:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.La ausencia de libicu muestra el mismo consejo con una línea inicial diferente: Libicu's dependencies is missing for Dotnet Core 6.0. Ambos mensajes proceden del mismo punto: config.sh ejecuta ldd contra las bibliotecas incluidas antes de iniciarse. Por eso, un enlace sin resolver detiene el script en lugar de provocar un fallo confuso más adelante.
Registra el runner en tu repositorio
Obtén un token del repositorio. Abre Settings, luego Actions, después Runners y, por último, New self-hosted runner. La página muestra un token de registro que comienza por A. Caduca una hora después de crearse, así que genéralo cuando estés listo para pegarlo.
Regístralo con el usuario del runner. config.sh no se puede ejecutar con sudo.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceEstas son las funciones de esas opciones. --name define cómo aparece el runner en el repositorio, así que elige un nombre que puedas reconocer dentro de seis meses. --labels añade tus propias etiquetas; el runner ya incluye self-hosted, Linux y X64 de forma predeterminada. --work define el directorio donde se guardan los checkouts, dentro del directorio del runner. --unattended responde a las solicitudes interactivas con sus valores predeterminados, que es lo que necesitas cuando el comando se ejecuta desde un script. --replace reutiliza un registro existente con el mismo nombre en lugar de fallar, que es lo que necesitas al reconstruir el servidor.
Una ejecución correcta termina con estas líneas:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.El registro se guarda en el directorio del runner como .runner, .credentials y .credentials_rsaparams. Los dos últimos identifican este runner ante GitHub, por lo que cualquiera que pueda leerlos puede suplantarlo. Por ese motivo, el directorio tiene el modo 700 y el usuario no tiene acceso a sudo.
Instalar el runner como servicio de systemd
./run.sh en una terminal sirve para una prueba, pero se detiene al cerrar la sesión SSH. Instala el servicio para que el runner se inicie durante el arranque. servicios y temporizadores de systemd en un VPS explica los propios archivos de unidad. Aquí, svc.sh crea uno automáticamente.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh requiere root porque escribe una unidad en /etc/systemd/system y la habilita. El argumento que sigue a install es el usuario con el que se ejecuta el servicio. Pasa gharunner explícitamente. Sin argumentos, el script usa $SUDO_USER, que es tu cuenta de administrador. En ese caso, todos los trabajos se ejecutan con un usuario que puede usar sudo.
La unidad recibe un nombre basado en el repositorio y el runner, con el formato actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. No tienes que escribirlo manualmente:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerUn runner operativo registra √ Connected to GitHub y después una línea que termina en Listening for Jobs. La página Runners del repositorio lo muestra como Idle. Un runner que aparece como Offline no se está ejecutando o no puede conectarse a GitHub por el puerto 443.
Enviar un trabajo al ejecutor
runs-on selecciona un ejecutor por etiqueta. Solicita self-hosted y añade tu propia etiqueta para impedir que el trabajo se ejecute en un ejecutor no previsto.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aSi el trabajo permanece en Waiting for a runner to pick up this job, las etiquetas no coinciden. Todas las etiquetas de runs-on deben existir en el ejecutor. Una palabra adicional deja el trabajo en cola sin mostrar ningún error. Compara la lista con las etiquetas que aparecen junto al ejecutor en la configuración del repositorio.
Por qué los runners autohospedados y los repositorios públicos no son compatibles
Esta es la parte que muchas personas omiten. Las directrices de GitHub son claras: los runners autohospedados «casi nunca deberían usarse para repositorios públicos» y «no ofrecen garantías de ejecutarse en máquinas virtuales efímeras y limpias, por lo que el código no confiable de un flujo de trabajo puede comprometerlos de forma persistente».
El mecanismo es sencillo. Una solicitud de incorporación de cambios de un fork incluye su propia copia del archivo de flujo de trabajo. Si el repositorio público ejecuta flujos de trabajo de solicitudes de incorporación de cambios en el runner, cualquiera que pueda crear un fork del repositorio puede proponer un flujo de trabajo que ejecute sus comandos en el VPS. No necesita permisos de escritura, porque lo que propone es lo que se ejecuta.
La configuración de aprobación reduce el riesgo, pero no lo soluciona. La directiva predeterminada para un repositorio público pide a un mantenedor que apruebe el flujo de trabajo del fork de un colaborador que participa por primera vez. Después de aprobar a esa persona una vez, sus solicitudes de incorporación de cambios posteriores se ejecutan sin una nueva solicitud de aprobación. Por tanto, el control consiste en que una persona revise un diff cada vez, y es fácil pasar por alto una carga maliciosa oculta tres niveles más abajo en un script de compilación.
Una solicitud de incorporación de cambios de un fork no recibe tus secretos y su GITHUB_TOKEN es de solo lectura. Esto limita los daños dentro de GitHub. No protege el servidor. El atacante obtiene un shell como gharunner, por lo que puede leer todos los archivos que ese usuario pueda leer, acceder a todo lo que el VPS pueda alcanzar en su red privada y dejar algo en ~/.bashrc o en una unidad de systemd del usuario que se ejecute durante el siguiente trabajo.
Registrar el runner con --ephemeral hace que acepte un trabajo y después se anule el registro, de modo que un trabajo no pueda leer el workspace del siguiente. Esto solo ayuda si algún proceso vuelve a crear la máquina o el contenedor para cada trabajo, porque una puerta trasera escrita en el directorio personal del usuario del runner sobrevive a un registro nuevo.
Las reglas siguientes son breves. Usa runners autohospedados para repositorios privados. Si debes asociar uno a un repositorio público, no ejecutes en él solicitudes de incorporación de cambios de forks, no mantengas nada más en ese servidor y trata la máquina como desechable.
Trabajos de Docker y el grupo que equivale a root
Los trabajos de contenedores, los contenedores de servicio y cualquier paso del flujo de trabajo que llame a docker build necesitan un daemon de Docker en el host del runner. Instala Docker de la forma habitual, descrita en Docker y Docker Compose en un VPS, y añade el usuario del runner al grupo docker.
Comprende el compromiso antes de hacerlo. Pertenecer al grupo docker equivale a tener acceso de root, porque un contenedor puede montar / mediante un bind mount y ejecutarse como root dentro de él. Por tanto, un flujo de trabajo que pueda comunicarse con el socket de Docker puede leer y escribir cualquier archivo del VPS, incluido /etc/shadow. En un repositorio privado con colaboradores de confianza, este riesgo puede ser aceptable. En cualquier otro caso, elimina la razón de usar un usuario sin privilegios. Rootless Docker mantiene las compilaciones de contenedores dentro de los permisos del propio usuario del runner, pero utiliza un controlador de almacenamiento más lento y no permite contenedores con privilegios.
Actualizaciones y eliminación correcta del runner
Un runner autohospedado se actualiza de forma predeterminada. Detecta una nueva versión, reemplaza sus propios archivos y reinicia el servicio, por lo que normalmente no debe hacer nada. ./config.sh --disableupdate desactiva la actualización automática cuando necesita una versión fija. Después, la actualización queda bajo su responsabilidad: la documentación de GitHub indica explícitamente que un runner configurado con --disableupdate debe actualizarse manualmente.
Una actualización manual conserva el registro, porque .runner y .credentials no están en el archivo tar. Detenga el servicio, descargue y compruebe la suma de comprobación del nuevo archivo tar como gharunner, extráigalo sobre el mismo directorio con tar xzf y vuelva a iniciar el servicio:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startPara eliminar el runner, desinstale primero el servicio y, después, elimine el registro. El token de eliminación se obtiene en la misma página Runners, en el botón Remove del propio runner.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HEREEliminar el directorio sin eliminar el registro deja el runner como Offline en el repositorio, porque GitHub solo sabe que ya no existe cuando el runner lo comunica o un administrador elimina la entrada manualmente.
Modos de fallo, con los mensajes que verá
Must not run with sudo. config.sh muestra este mensaje y termina cuando se ejecuta como root. La comprobación es intencionada, porque los archivos propiedad de root en _work interrumpen todos los trabajos posteriores que se ejecutan con el usuario del servicio. Ejecute ./config.sh como gharunner. La variable RUNNER_ALLOW_RUNASROOT omite la comprobación, pero usarla solo retrasa el fallo.
sudo: ./svc.sh: command not found. Está en el directorio correcto. svc.sh aún no existe porque config.sh no ha completado ningún registro. Registre el runner y, después, instale el servicio.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. El token no es un token de registro válido. Puede haber caducado, ya que solo es válido durante una hora, o puede que se haya pegado un personal access token en lugar del token de registro de la página Runners. Genere un token nuevo y vuelva a pegarlo.
Dependencies is missing for Dotnet Core 6.0. Ejecute sudo ./bin/installdependencies.sh desde el directorio del runner como root y vuelva a registrarlo.
Runner sin conexión después de reiniciar. Ejecute systemctl is-enabled 'actions.runner.*'. Si no aparece nada, nunca se ejecutó ./svc.sh install, por lo que el runner solo existió dentro de la sesión de terminal. Si la unidad está habilitada y el runner sigue sin conexión, lea journalctl -u 'actions.runner.*' y compruebe HTTPS saliente.
El disco se llena. Los checkouts, las cachés de compilación y las imágenes de Docker se acumulan en _work y en el directorio principal del usuario del runner, y ningún proceso los elimina automáticamente. Supervise du -sh /home/gharunner/actions-runner/_work y añada una limpieza programada antes de que el disco se llene.
FAQ
¿Por qué sudo ./svc.sh install muestra «command not found»?
Porque svc.sh no está en el archivo tar del runner. Se genera en el directorio del runner cuando ./config.sh termina el registro, usando el repositorio y el nombre del runner para crear el nombre del servicio. Ejecute primero ./config.sh como el usuario del runner. Después, sudo ./svc.sh install gharunner encuentra el script y escribe una unidad llamada actions.runner.OWNER-REPO.RUNNER-NAME.service en /etc/systemd/system.
¿Necesito abrir un puerto del firewall para un runner autoalojado?
No. El runner abre una conexión HTTPS saliente a GitHub y la mantiene abierta mientras espera trabajos. Por eso, GitHub nunca inicia una conexión con su VPS. Permita las conexiones salientes por el puerto 443 y mantenga cerradas las reglas entrantes. Si el runner muestra Offline mientras su servicio está en ejecución, revise el filtrado de conexiones salientes y DNS, no las reglas entrantes.
¿Puedo usar un runner autoalojado en un repositorio público?
Puede hacerlo, pero GitHub no lo recomienda. Una solicitud de incorporación de cambios desde un fork incluye su propio archivo de workflow. Por lo tanto, cualquiera que pueda crear un fork de su repositorio puede proponer comandos que se ejecuten en su máquina. El aviso de aprobación solo cubre la primera ejecución de un colaborador. Si asocia un runner a un repositorio público, desactive los workflows de solicitudes de incorporación de cambios desde forks en ese runner, no mantenga nada más en ese servidor y reconstruya la máquina periódicamente.
¿Por qué falla el registro con Http response code: NotFound?
La llamada de registro devuelve NotFound cuando la credencial es incorrecta, no solo cuando la URL es incorrecta. Esto hace que el mensaje sea engañoso. Los tokens de registro caducan una hora después de mostrarse, y no se acepta un personal access token para esta llamada. Abra Settings, Actions, Runners, New self-hosted runner de nuevo, copie el token nuevo y confirme que el valor de --url apunta a un repositorio en el que tiene derechos de administrador.