1 Security Model
mitosis edited this page 2026-09-02 08:56:51 +00:00

What a tenant can and cannot do on this platform. Each boundary is a guarantee, and each guarantee names the mechanism that enforces it. Enforcement is structural — Forgejo permissions, branch protection, a rootless CI pool, image provenance, and network policy — never policy on paper.

The boundaries

The table lists each guarantee with the mechanism that enforces it.

Guarantee Mechanism
CI code cannot reach the node, other tenants, or the cluster API A non-privileged, isolated runner pool: no Docker daemon, automountServiceAccountToken: false, an egress-allowlist NetworkPolicy
CI code cannot reach cloud metadata or the private network Egress denies RFC1918 (10/8, 172.16/12, 192.168/16) and 169.254.169.254/32
A workflow cannot push another org's images Forgejo delegates package write only through an org team; REGISTRY_TOKEN is a per-org write:package-only credential
A workflow's token works on exactly one repo github.token is an ephemeral per-run token, scoped to that repo
An agent cannot write outside its assigned repo A per-run SSH deploy key covers one repo; the platform deletes it when the run exits
Tenants cannot enumerate each other Orgs and repos are private by default; org membership is hidden

The CI boundary

All CI runs on one pool. Runner jobs are ephemeral, scale from zero, and claim runs-on: isolated. The pod shape is the security boundary:

  • No Docker daemon, anywhere. Steps run in host mode inside the toolchain container. Image builds go through a rootless BuildKit sidecar (runAsUser: 1000, privileged: false) over a UNIX socket. No privileged pool exists. A runs-on: docker job is never claimed by anything; it waits forever.
  • No cluster credentials. automountServiceAccountToken: false keeps the kube API token away from tenant code.
  • An egress-only allowlist NetworkPolicy. It permits DNS, the Forgejo namespace (clone and registry pull), traefik (HTTPS registry push), and the MCP endpoints /ci/preview and /ci/sync-env. Those endpoints authorize each call by a check of the caller's Actions token for push rights, and they fail closed. Public internet egress is open, except RFC1918 ranges and the cloud-metadata address 169.254.169.254/32.
  • A bounded lifetime. Each runner pod registers ephemerally, serves one job, and stops at a hard deadline.

The toolchain inside the pool is covered in CI and Builds.

The workflow surface

You author any workflow you like in your app repos. A small set of controls keeps that open surface safe:

  • guard-ppt. A required status check (guard-ppt / no-ppt*) fails any PR whose workflows use pull_request_target — the trigger that hands base-repo secrets to untrusted PR code. The check is required on open-platform/mitosis and the app template today. Making it required on every app repo is an open follow-up.
  • Fork PRs run without secrets. A fork's pull_request run gets an empty secrets map. An untrusted fork author needs manual approval before anything runs. The base branch's workflow definition runs — not the fork's.
  • The one host-executing workflow is actor-gated. The coldstart gate runs PR code directly on a host, so it admits only an explicit allowlist of trusted actors. The gate keys on the actor, not the fork, because an AGit PR places its head in the base repo.
  • System repos lock their workflow files. A branch-protection rule named ** carries protected_file_patterns: .forgejo/workflows/**;.gitea/workflows/**;.github/workflows/**. A non-admin pusher — including every agent deploy key — cannot add or alter CI workflows on any branch. New app repos skip this rule on purpose, so you keep workflow authorship.
  • The merge gate. The required check / check* status gates every merge to main.

Credential scope

  • github.token in a workflow is Forgejo's ephemeral per-run token. It answers 200 on its own repo and 404 on every other repo, same-org included.
  • REGISTRY_TOKEN belongs to registry-bot-<org>: a restricted user in a packages-only team (registry-writers, units_map: {"repo.packages": "write"}) with a write:package-only token. This delegation model is also why apps must be org-owned — Forgejo delegates package write only via an org team.
  • Agents push with a per-run read-write deploy key. The key covers exactly one repo, and the platform deletes it when the run exits. The key is non-admin, so branch protection binds on it.
  • Platform bots are restricted users with narrow write scopes. No admin credential sits within tenant reach.
  • Self-serve secrets are write-only. A read of a single value returns HTTP 405. The sync-env pipeline drops reserved names (REGISTRY_TOKEN, FORGEJO/GITEA/GITHUB_TOKEN, PLAT_*, APP_ENV) before it seals anything into your app. See Secrets and Config.

The app namespace

Every app namespace is fenced independently of CI. pod-security.kubernetes.io/enforce: restricted applies to every pod. Kyverno generates a ResourceQuota, a LimitRange, and default-deny NetworkPolicies, keyed off the plat.sh/app label with self-healing sync. Twelve ClusterPolicies enforce — among them registry restriction, image-provenance verification before deploy, and a ban on admin ClusterRoleBindings. The one exception is the forwardauth-on-app-ingress policy, which is not yet enforced. The full roster is in Platform Internals.

Privacy defaults

New repos and orgs are private by default (DEFAULT_PRIVATE: private, DEFAULT_ORG_VISIBILITY: private). DEFAULT_ORG_MEMBER_VISIBLE: false closes org and org-package enumeration. The bulk users page is disabled. Anonymous browsing shows only the public seed repos. Where registration is open, it uses a sovereign image CAPTCHA — no third-party service — and internal accounts only. A new member signs in and sees exactly their own orgs, nothing else. Roles and teams are covered in Users Teams and Access.