Skip to content
SRE & DevOpsDeep Dive Published Updated 6 min readViews unavailable

Gitea Actions Runner Security: Isolate act Runner Jobs and Scope Their Tokens

Design Gitea Actions around runner trust, container and host boundaries, ephemeral execution, GITEA_TOKEN permissions, and safe action dependencies.

Gitea Actions brings workflow automation into a Gitea instance, but it does not make the Gitea server an execution sandbox. Jobs are assigned to a separately operated runner, act_runner, which downloads workflow/action code, executes it, and can receive repository secrets and a job token. The runner’s host, container engine, network access, caches, registration credentials, and job token are all part of the CI trust boundary.

Gitea describes Actions as similar to, and mostly compatible with, GitHub Actions. “Mostly compatible” matters: workflows, action distribution, token scopes, event behavior, runner images, and service integrations should be validated against the exact Gitea and runner versions you operate. Do not assume that a workflow tested on GitHub has identical security semantics on a self-hosted Gitea instance.

Decide which code the runner is allowed to execute

Before choosing runner scope, classify the repositories and contributors that can submit workflow changes. A repository-level runner narrows which repository may schedule work, but it is not isolation if that repository contains untrusted contributors, unreviewed dependencies, or workflows that can execute arbitrary shell commands. Organization- or instance-level runners require even stronger separation because a single malicious workflow can target shared host state, credentials, or caches.

The Gitea documentation explicitly warns that a runner should not be used by an instance you do not trust, and that a runner should not be offered to repositories or instances you do not trust. For public repositories or mixed-trust organizations, prefer short-lived runners on disposable VMs or containers. Where supported by the runner version, ephemeral registration is a way to limit the lifetime of runner credentials around one job; it is not a substitute for isolating the machine on which untrusted code executes.

Avoid placing production deployment credentials on a general-purpose runner. A build job that compiles pull-request code should not share a persistent host, writable image cache, workspace volume, or cloud identity with a production release runner. Separate pools by trust tier and purpose, not merely by operating system label.

Understand what Docker isolation does and does not mean

The runner documentation offers host, Docker, and Docker-in-Docker execution modes. Running the runner on the host provides no job encapsulation. Docker mode runs jobs in containers, which can improve separation but still depends on the daemon, mounts, network, kernel, and runner configuration. Docker-in-Docker can provide a rootless arrangement with fewer privileges according to Gitea’s runner guidance, but it is more complex and still must be evaluated as a concrete boundary rather than assumed secure by its name.

The most important red flag is mounting the host’s Docker socket into a runner or job. Access to that socket can let job code control the host Docker daemon and potentially create privileged containers or inspect other containers. Do not expose /var/run/docker.sock to untrusted workflow code. If a runner design needs a container engine, isolate that engine inside the disposable worker or VM, constrain capabilities and mounts, and destroy the worker after use.

Use runner labels deliberately. A label decides which execution mode and image handle a job; it is not an authorization policy. Do not use a generic label such as ubuntu-latest for both isolated PR work and a production-only host executor. Create labels that make the trust purpose explicit, and test how unmatched labels are handled so a configuration mistake cannot silently fall back to a host-capable runner image.

Clamp the built-in job token

Every Actions job gets a built-in GITEA_TOKEN that can access Gitea APIs and Git operations. Gitea supports a subset of GitHub Actions permissions: scopes and computes the token from job-level permissions, workflow-level permissions, and configured defaults. The effective scope is clamped by the configured maximum. If no explicit permission block is provided, the instance’s configured default applies; the documented backward-compatible default is permissive, while restricted mode is read-only for code, releases, and packages and denies other units by default.

Set organization or instance defaults to restricted, set a maximum permission ceiling, and declare the minimum required token permissions at the job or workflow level. Do not assume that narrowing GITEA_TOKEN also narrows a separately supplied personal access token, cloud key, or other secret. Those values have their own permissions and lifetime. Avoid using a long-lived PAT where a short-lived federated identity or narrowly scoped deploy token is available.

Fork pull requests are documented as read-only for repository contents regardless of requested workflow permissions or settings. Keep that boundary, but also prevent private secrets from being passed into jobs that execute fork-controlled code. A read-only token does not make arbitrary code safe if the runner host has access to other credentials or internal services.

name: Test

on:
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: isolated-linux
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/test.sh

The example uses a conventional workflow shape only. Confirm permissions support and action behavior against the Gitea version in use, and pin external actions to an immutable reviewed reference where possible. The actions/checkout name resolves according to Gitea’s configured default action source; it is not proof that code came from GitHub or that the referenced code is trustworthy.

Protect registration and cache state

Treat runner registration tokens and the persisted .runner registration file as credentials. Store tokens outside version control, restrict filesystem permissions, reset a token if it may have leaked, and do not print environment variables or runner configuration in job logs. A registration token may authorize multiple runner registrations until it is reset, so it is not a disposable per-job secret unless your setup explicitly issues it that way.

Caches improve throughput but cross-trust reuse can leak private build products or let an untrusted job poison a later privileged build. Partition cache backends and keys by repository and trust boundary; do not let untrusted fork jobs write entries consumed by production release workflows. Apply retention and size limits, and treat cache restore as an optimization, not as integrity evidence for a release.

Validate the actual runner boundary

Inventory runner scope, host mode, runner image, labels, container socket mounts, network reachability, cache backend, registration state, token defaults, maximum permissions, and attached secrets. Run a controlled untrusted test job to confirm it cannot read host credentials, access the container daemon, or reach internal endpoints it should not. Run a token test that can read but cannot write a repository object under the restricted policy. Then test the production job with only the intended service identity and verify its cloud or deployment audit event.

Upgrade Gitea and act_runner on a planned cadence, reviewing compatibility before changing either side. Monitor runner registration, job assignment, container/image pulls, cache errors, and permission denials. A green job status is not a security test: the useful evidence is that code ran in the intended isolation boundary with only the declared permissions.

Related:

Sources:


Comments