Files
ipcr-forge/Apps/Gitea/rationale.md
T
2026-10-03 16:09:47 +02:00

4.6 KiB

Gitea — Rationale

What deviation / exception is being requested

  1. gitea-runner runs with privileged: true.
  2. Gitea is configured with [migrations] ALLOW_LOCALNETWORKS = true.
  3. The admin account is called gitea_admin, not the deployment's usual admin.

Why it is necessary

1. The privileged runner. The point of shipping a runner at all is that a workflow can build a container image. That needs a Docker daemon, and there are only two ways to give a CI job one:

  • mount the host's /var/run/docker.sock into the runner, which hands every workflow — including one that arrives from a mirrored third-party repository — full control of the host's daemon, i.e. root on the PCS; or
  • give the runner a daemon of its own inside the container (dind), which requires privileged.

The second is the smaller exposure: the blast radius is the runner's own container and its own image store, not the host's containers, volumes and socket. It is also what upstream ships the -dind tag for.

The rootless dind variant (0.6.1-dind-rootless) would have avoided privileged altogether, and was tried first. It does not start on this platform: Ubuntu sets kernel.apparmor_restrict_unprivileged_userns=1, and rootlesskit dies with fork/exec /proc/self/exe: operation not permitted. Making it work means turning that protection off for the container (security_opt: apparmor=unconfined), which weakens the host more than privileged on a single container does. Revisit if the platform ever ships that sysctl relaxed, or if upstream's runner learns to use the host's user-namespace remapping.

2. ALLOW_LOCALNETWORKS. Gitea refuses by default to migrate or mirror from an address on a private network. The app's most useful configuration — mirroring a repository from another app on the same server, e.g. a Radicle node's git endpoint at http://radicle-api:8080/<rid>.git — is exactly that case. Without the flag the URL is rejected before any fetch is attempted.

3. The admin username. admin is on Gitea's reserved-name list (it collides with the /admin route) and account creation fails with it.

Security mitigations in place

  • The runner is not on the shared pcs network. It sits on an app-internal bridge with Gitea and nothing else, so no other app can reach it, and it publishes no port.
  • The runner's daemon is nested: its image store is a directory under the app's own AppData, and the containers it creates are invisible to, and cannot see, the host's daemon.
  • capacity: 1 in the runner config and a 2G memory limit bound what a runaway build can consume.
  • Self-registration is disabled (DISABLE_REGISTRATION) and anonymous browsing is off (REQUIRE_SIGNIN_VIEW), so the forge starts closed with a single account whose password is the deployment's default app password.
  • ALLOW_LOCALNETWORKS is reachable only by an authenticated user of this Gitea — with registration disabled, that is the owner. It widens what the owner can point a mirror at; it does not expose anything to an anonymous caller.
  • The runner is severable: stopping the gitea-runner container leaves a fully working forge and removes the privileged container from the box. This is called out in tips.before_install.

Alternatives considered and rejected

Alternative Why not
Host Docker socket in the runner Any workflow, including one from a mirrored repo, gets root on the host. Strictly worse than a privileged container.
0.6.1-dind-rootless Does not start under Ubuntu's unprivileged-userns restriction (see above); the workaround disables AppArmor for the container.
Ship no runner Gitea Actions is the reason to choose Gitea over a plain git server here, and a runner cannot be added later without the same privileged decision.
Mirror over the public HTTPS address instead of ALLOW_LOCALNETWORKS Works, but routes a same-box fetch out to the gateway and back, and breaks if the deployment has no public address.

Data protection

Everything that survives a restart is under /DATA/AppData/gitea/: repositories and the SQLite database in data/, the rendered app.ini in config/, the runner's registration in runner/, and the nested daemon's image store in runner-docker/. Nothing is written outside that tree, and no user directory (/DATA/Documents, /DATA/Media, …) is mounted into any container.

The only credential the app generates is the runner registration token, which is rewritten on every start and is worthless without network access to Gitea. The admin password is the deployment's own APP_DEFAULT_PASSWORD; it is never written to disk by this app beyond Gitea's own salted hash.