Gitea 28: novedades y precauciones en la actualización

Mascote LinuxPro abre uma caixa com o logo do Gitea e símbolos de auditoria, bots e segurança, acompanhado pelo caramelo ciborgue.

Gitea saltó de la 1.27 a la 28 — y no, no fueron 27 versiones perdidas. La 28.0.0, anunciada el 30 de septiembre de 2026, es la versión que se llamaría 1.28.0: el proyecto simplemente abandonó el “1.” que nunca avanzaba. Tras el nuevo número hay un release grande, con log de auditoría, cuentas de bot, deploy tokens, impersonación por parte del administrador y varias mejoras en las Actions — y cambios incompatibles que exigen revisar seguridad, retención y workflows antes de actualizar. Este post resume lo que cambió y cuenta lo que aprendimos actualizando una instancia de producción.

Por qué 28 y no 1.28

Desde la bifurcación de Gogs, en 2016, Gitea numeraba las versiones como 1.x. El primer número nunca cambió; el que llevaba la información era el segundo. A partir de esta versión, ese segundo número pasó al frente: 1.27 → 28. No hay reescritura ni una ruptura general de compatibilidad por culpa del número — hay cambios incompatibles que hay que evaluar, descritos más abajo.

El efecto práctico se nota en la automatización: scripts que buscan etiquetas, 1.*, comparan versiones como texto o construyen la URL de descarga a mano. Junto con el cambio, el proyecto 28 dejó de publicar binarios x86 de 32 bits y las variantes gogit, y los nombres de archivo perdieron el sufijo de versión del sistema operativo. Para Linux amd64, el binario sigue en https://dl.gitea.com/gitea/28.0.0/gitea-28.0.0-linux-amd64, con .sha256, firma GPG y paquete Sigstore al lado.

Log de auditoría

Una característica importante para quien administra Gitea en una empresa. Los eventos relevantes de seguridad pasan a registrarse al estilo de GitHub: acción, autor, ámbito, origen (interfaz, API, CLI o sistema) y metadatos. Los eventos aparecen en los ajustes del administrador, de la organización, del repositorio y del usuario, con filtros. Las acciones realizadas durante una suplantación registran a ambas personas.

El detalle que importa: la grabación viene desactivada. Para activarla, en app.ini:

[audit]
RECORD_OUTPUT  = database
RETENTION_DAYS = 90   ; padrão 30; 0 guarda para sempre

La limpieza de los eventos antiguos la realiza la tarea cron.delete_old_audit_events.

Cuentas de bot y deploy tokens

La automatización en Gitea solía significar crear un usuario normal, generar un token y esperar que nadie iniciara sesión con él. La 28 trae cuentas de bot de verdad: usuarios sin contraseña, que solo lo hacen mediante token, no reciben notificaciones ni correos y no pueden iniciar sesión interactiva — ni por login, ni por proxy inverso, ni por origen externo. Pueden administrarse desde la interfaz, la API o la CLI. El comando gitea admin user change-type convierte una cuenta local existente en bot o realiza la conversión inversa.

Los tokens de despliegue son la pareja de las deploy keys para HTTPS: una credencial limitada a un repositorio, con lectura o lectura y escritura, usada como contraseña en una operación Git — incluyendo LFS. Sirven para que el servidor de producción descargue un repositorio sin clave SSH y sin la cuenta de una persona.

Completan el paquete los tokens personales regenerables: se puede cambiar el valor de un token manteniendo nombre y permisos, útil cuando se ha filtrado o se ha entregado a terceros — el propio PR cita el caso de un token pasado a un agente de IA.

Administración y revisión de código

  • Suplantación: el administrador puede ver la instancia como un usuario concreto para investigar un problema de permisos. La acción queda registrada cuando el registro de auditoría está habilitado.
  • Code owners obligatorios: una nueva regla de protección de rama exige la aprobación de un propietario o integrante del equipo por regla correspondiente en el CODEOWNERS.
  • Diff más navegable: búsqueda y filtro por extensión en la barra lateral de archivos, y líneas largas truncadas, pero visibles.
  • Notificaciones por WebSocket: el canal de eventos en tiempo real (contador de notificaciones, cronómetro, logout) cambió SSE por WebSocket en /-/ws. Si el WebSocket no conecta, la interfaz vuelve a consulta periódica.

Gitea Actions

Las Actions — el CI/CD integrado, con YAML compatible con el de GitHub Actions — ganaron:

  • Cola de builds: una vista de solo lectura de los jobs, con los en ejecución primero y después los que están esperando, en el orden en que un runner los tomará. Aparece en la administración para la instancia completa y en cada repositorio.
  • Matriz dinámica: o strategy.matrix de un job se puede montar a partir de las salidas de jobs anteriores.
  • max-parallel en la matriz, cancelación forzada de ejecuciones por la API y más endpoints para gestionar ejecuciones y registros.
  • Vista previa de artefactos directamente en la página de la ejecución.

En el lado del ejecutor, el runner llegó a 4.1.0 el 1 de octubre, con un backend para ejecutar los trabajos en Kubernetes.

Retención y egress: dos precauciones antes de la actualización

Empieza por la retención de datos y los permisos de salida. Hay otros requisitos de compatibilidad en la siguiente sección.

1. El historial de Actions pasa a expirar

Hasta 1.27, las ejecuciones completadas se quedaban en la base de datos para siempre. 28 crea el RUN_RETENTION_DAYS, con patrón de 400 días: en la limpieza de la medianoche siguiente a la actualización, las ejecuciones más antiguas que eso se borran, junto con los trabajos, logs y artefactos. Sin una copia de seguridad, no hay restauración automática. Para mantener el comportamiento antiguo, define antes de actualizar:

[actions]
RUN_RETENTION_DAYS = 0   ; 0 = guardar para sempre

Atención al 0: ahora también significa “guardar para siempre” en LOG_RETENTION_DAYS e ARTIFACT_RETENTION_DAYS. Hasta 1.27, quien pusiera 0 en esas dos claves tenía los logs y artefactos borrados en la limpieza siguiente.

2. El tráfico Git de salida pasa por un proxy interno

Las migraciones, los espejos y otras operaciones de red de Git ahora pasan por un proxy interno, que aplica las reglas de egress a las conexiones directas. Las reglas han ganado un modo:

  • EGRESS_MODE = lax (por defecto): permite hosts públicos en cualquier puerto, salvo bloqueos explícitos; las direcciones privadas y de loopback requieren permiso.
  • EGRESS_MODE = strict: solo libera lo que esté en la lista de permitidos, respetando los bloqueos; las entradas sin puerto valen únicamente para 80 y 443.
Fluxo das operações Git de migração e espelhamento pelo proxy interno, comparando permissões de saída nos modos lax e strict; a lista de bloqueio prevalece
Conexiones Git directas: el modo strict restringe los destinos a la lista de permitidos. Los bloqueos explícitos prevalecen en ambos modos.

La alerta de las notas de versión es específica: en [security], una ALLOWED_HOST_LIST que antes restringía los destinos públicos deja de ser exclusiva en el modo predeterminado lax. Para mantener esa restricción en webhooks y OAuth2, configure EGRESS_MODE = strict. No copie esa configuración sin enumerar los destinos realmente necesarios.

Hay una excepción importante en la migración de configuración: en [migrations], si la lista nueva está ausente, una ALLOWED_DOMAINS heredada mantiene el modo estricto por compatibilidad y permite todos los puertos de los hosts listados. Al adoptar la lista nueva, revise los puertos explícitamente. Consulte la referencia de migraciones, en lugar de asumir equivalencia entre claves antiguas y nuevas.

Las listas controlan conexiones directas. Si hay un proxy de salida configurado, el bloqueo de los destinos reenviados pasa a ser responsabilidad de ese proxy.

Además:

  • El preajuste external ha dejado de existir.
  • Comodines en direcciones IP y la entrada * ya no se aceptan.
  • Los dominios siguen la sintaxis de curl: example.com se aplica al dominio y a todos los subdominios; *.example.com, solo a los subdominios.
  • Una entrada no válida en [migrations] BLOCKED_HOST_LIST impide que Gitea arranque.
  • ALLOWED_DOMAINS, BLOCKED_DOMAINS e ALLOW_LOCALNETWORKS quedan obsoletas a favor de ALLOWED_HOST_LIST e BLOCKED_HOST_LIST.
  • O [migrations] controla migraciones y espejos; el [security], webhooks y OAuth2.

Otros cambios incompatibles

  • Git mínimo: la versión 28 requiere Git 2.25.0 o posterior. Compruébalo git --version en el entorno que ejecuta Gitea.
  • Registro: el autorregistro está desactivado por defecto. Para permitirlo de forma deliberada, configura [service] DISABLE_REGISTRATION = false.
  • URL de la instancia: [server] DOMAIN deja de leerse; revisa ROOT_URL, también se usa para derivar el dominio SSH por defecto.
  • Workflows: o if: del job se evalúa antes de la expansión de la matriz. Las condiciones con matrix deben moverse a pasos o a la configuración de la matriz.
  • Fallo de matriz: strategy.fail-fast pasa a aplicarse; configura false cuando todas las combinaciones tengan que terminar.
  • Workflows reutilizables: los repositorios públicos no pueden llamar a workflows privados, y las llamadas anidadas no pueden elevar los permisos del token del llamante.

Estos puntos forman parte de los cambios incompatibles documentados por el proyecto.

En la práctica: actualizando una instancia de producción

En la actualización de una instancia instalada como binario con systemd y MySQL, con runner propio y espejos, tres puntos merecieron atención.

La configuración heredada de migraciones merecía revisión. O app.ini tenía:

[migrations]
ALLOW_LOCALNETWORKS = true
ALLOWED_DOMAINS     = github.com,api.github.com,gitlab.com

Una configuración explícita con la lista nueva puede usar el modo estricto. No es una equivalencia exacta: las entradas sin puerto pasan a permitir solo 80 y 443. Además, github.com ya cubre api.github.com:

[migrations]
EGRESS_MODE       = strict
ALLOWED_HOST_LIST = github.com,gitlab.com,private,loopback

El ejemplo permite redes privadas y loopback: elimine esos preajustes si no son necesarios y prefiera destinos específicos. Verifique también de dónde proceden los espejos; en el modo estricto, un Git interno en un puerto como 3000 necesita una entrada con el puerto, como 10.0.0.15/32:3000 (sustituya por la IP real de su entorno). Valide la configuración y el acceso a los destinos antes del cambio.

El historial de Actions sería parcialmente borrado. Con el patrón de 400 días, las ejecuciones por encima de ese plazo se eliminarían en la limpieza programada. Definimos RUN_RETENTION_DAYS = 0 antes de la actualización.

El nginx no reenviaba el WebSocket. Tras la actualización, el handshake en /-/ws respondía 101 Switching Protocols directamente en Gitea, pero 426 Upgrade Required a través del nginx. El bloque de proxy sí reenviaba las cabeceras Upgrade e Connection, pero faltaba la versión del protocolo — en nginx 1.18.0 verificado, el valor por defecto para el upstream es HTTP/1.0, que no permite ese upgrade:

Para una configuración nueva, el ejemplo siguiente utiliza el map recomendado en la documentación de nginx. El map pertenece al contexto http; el location, al bloque server existente:

# Dentro de http {}, fora de server {}
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# Dentro do server {} existente
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;              # a linha que faltava
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Para probar, sin navegador:

curl -s --max-time 5 -o /dev/null -w '%{http_code}\n' --http1.1 \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  https://git.exemplo.com.br/-/ws
# 101 = handshake concluído; 426 = investigar o upgrade no proxy

Después de recibir 101, la conexión permanece abierta; el timeout y el código de salida 28 de curl al cabo de los cinco segundos son esperados en esta prueba. Valide la configuración con nginx -t antes de recargar el servicio.

Las notificaciones siguen funcionando si se olvida — la interfaz cae a consulta periódica —, pero las notificaciones dejan de ser instantáneas. Quien usa el Caddy delante no necesita hacer nada: el reverse_proxy gestiona WebSocket sin configuración adicional.

En el proxy nginx 1.18.0 verificado, añadir proxy_http_version 1.1, manteniendo los encabezados de upgrade ya existentes, cambió la respuesta de 426 para 101. La página de inicio continuó respondiendo 200 después de la recarga.

Ruta de actualización

  1. Copia de seguridad consistente: interrumpa las escrituras conforme a la documentación de copia de seguridad. Preserve la base de datos, los repositorios, la configuración y los archivos de datos en el mismo punto consistente; use el volcado nativo de la base de datos cuando se indique y pruebe la restauración. La migración de la base de datos no tiene rollback automático.
  2. Compatibilidad: compruebe Git, ROOT_URL, el registro y los workflows antes de reiniciar.
  3. Retención: decida la RUN_RETENTION_DAYS antes de subir 28.
  4. Egress: si usas ALLOWED_DOMAINS, BLOCKED_DOMAINS, ALLOW_LOCALNETWORKS o el preajuste external, reescribe con ALLOWED_HOST_LIST, BLOCKED_HOST_LIST e EGRESS_MODE. Si la lista debe ser exclusiva, usa strict.
  5. Scripts: ajusta lo que monta URL de descarga o compara versiones con “1.”.
  6. Cambio: sustituye el binario o la etiqueta de la imagen (docker.gitea.com/gitea:28.0.0) y reinicia. Comprueba la suma SHA-256 antes.
  7. Verificación: /api/healthz, la versión en /api/v1/version, los avisos en el log de inicio y la prueba de WebSocket arriba.

El proceso completo de instalación — Docker Compose con MariaDB, binario con systemd, runner y tea CLI — está en la guía Gitea: Git self-hosted con Actions, runner y tea CLI, ya actualizada para la 28.

Seguridad

La 28 incluye correcciones de seguridad, entre ellas el rechazo de objetos Git no válidos o duplicados en el push, la identificación de claves SSH mediante huella digital, la garantía de que las ejecuciones de PR provenientes de fork sigan esperando aprobación incluso después de canceladas, una corrección de denegación de servicio en SSH y la comprobación de autorización por repositorio en accesos de equipos, exclusiones y paquetes. Los detalles completos — con identificadores y gravedad — se publicarían aproximadamente una semana después del lanzamiento y, hasta el 5 de octubre de 2026, aún no se habían publicado. Esto, de por sí, ya es motivo para no retrasar la actualización.

¿Vale la pena actualizar?

Sí, después de revisar los cambios incompatibles y contar con un backup restaurable. La 28 es una release de maduración: auditoría, bots, deploy tokens y aprobación por code owners amplían los controles disponibles para los equipos, sin perder lo que siempre ha sido su ventaja — un binario, una base de datos y poca memoria. El nuevo número asusta más que la actualización.

Lee también: la historia de GitLab, Gogs, del que proviene Gitea e DevOps: ¿qué es CI/CD?.

Referencias: release 28.0.0, configuraciones de Gitea e runner 4.1.0. Revisado el 5 de octubre de 2026.