What a tenant on this platform can and cannot do — stated as guarantees, each with the mechanism that enforces it. Nothing here is policy-on-paper: every guarantee is enforced by construction, and the load-bearing ones were probed from a hostile throwaway account before registration opened.
The guarantees at a glance
| Guarantee | Mechanism |
|---|---|
| CI code cannot reach the node, other tenants, or the cluster API | Non-privileged isolated runner pool: no Docker daemon anywhere, automountServiceAccountToken: false, 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 | Package write is delegatable 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, strictly repo-scoped |
| An agent cannot write outside its assigned repo | Per-run SSH deploy key scoped to one repo, deleted on run exit |
| Tenants cannot enumerate each other | Orgs and repos are private by default; org membership is hidden |
The CI pool cannot escape
All CI runs on one pool: ephemeral, scale-from-zero runner jobs that 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. The privileged docker-in-docker pool that once existed is deleted — its escape path (DOCKER_HOST tcp://localhost:2375→docker run --privileged -v /:/host→ node root) is documented in the gitops repo as the reason. Aruns-on: dockerjob is never claimed by anything; it waits forever. - No cluster credentials.
automountServiceAccountToken: false— untrusted tenant code gets no kube API token. - Egress-only allowlist NetworkPolicy. DNS, the Forgejo namespace (clone + registry pull), traefik (https registry push), and the MCP service's
/ci/preview+/ci/sync-envendpoints — which authorize by probing the caller's Actions token for push rights, fail-closed. Public internet is allowed except RFC1918 ranges and the cloud-metadata address169.254.169.254/32. - Bounded lifetime. Runner pods are per-job, pre-registered ephemerally, and killed at a hard deadline.
Details of the toolchain itself are in CI and Builds.
Workflows are the attack surface — by design
You can author any workflow you like in your app repos. That is a deliberate, red-team-proven decision, held safe by a small set of controls rather than by locking you out:
guard-ppt— a required status check (guard-ppt / no-ppt*) fails any PR whose workflows usepull_request_target, the secrets-exfiltration foot-gun that instance config cannot ban. It is required on plat/mitosis and the app template today; making it required on every app repo is an open follow-up.- Fork PRs are defanged. A fork's
pull_requestrun gets an empty secrets map, untrusted fork authors need manual approval before anything runs, and the base branch's workflow definition runs — not the fork's. - The one host-executing workflow is actor-gated. The coldstart gate (which runs PR code directly on a host) admits only an explicit trusted-actor allowlist — actor-based rather than fork-based, because AGit PRs place their head in the base repo and would pass a fork check.
- System repos lock their workflow files. A branch-protection rule named
**carriesprotected_file_patterns: .forgejo/workflows/**;.gitea/workflows/**;.github/workflows/**, so a non-admin pusher — including every agent deploy key — can never add or alter CI workflows on any branch. New app repos deliberately skip this rule so you keep workflow authorship. - Merges are gated by the required
check / check*status on main.
Credentials are scoped by construction
github.tokenin a workflow is Forgejo's ephemeral per-run token (synthetic userforgejo-actions): 200 on its own repo, 404 on every other repo — same-org included. Hard-tested.REGISTRY_TOKENbelongs toregistry-bot-<org>: a restricted user in a packages-only team (registry-writers,units_map: {"repo.packages": "write"}), holding awrite:package-only token, with an HMAC-derived password that is never stored. 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 scoped to exactly one repo and deleted when the run exits. The key is non-admin, so branch protection binds on it.
- Platform bots are restricted users — e.g.
ci-botcarrieswrite:repository,write:issueonly. No admin credential sits within tenant reach. - Self-serve secrets are write-only (reading a single value returns HTTP 405), and the sync-env pipeline drops reserved names (
REGISTRY_TOKEN,FORGEJO/GITEA/GITHUB_TOKEN,PLAT_*,APP_ENV) before sealing anything into your app. See Secrets and Config.
What your app runs inside
Every app namespace is fenced independently of CI: pod-security.kubernetes.io/enforce: restricted, plus Kyverno-generated ResourceQuota, LimitRange, and default-deny NetworkPolicies keyed off the plat.sh/app label with self-healing sync. Twelve ClusterPolicies with the enforce flip active — including registry restriction, image-provenance verification, 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 signing in sees exactly their own orgs and nothing else. Roles and teams are covered in Users Teams and Access.
The red-team record
Registration opened only after both instance-takeover paths were closed and re-probed from a hostile throwaway account. The probes and their results:
| Probe | Result |
|---|---|
Claim a privileged CI job via runs-on: docker |
Never claimed — the privileged pool is deleted; the job waits forever |
| Extract an admin credential from CI | REGISTRY_TOKEN is per-org write:package-only; no admin PAT reachable |
Push packages with github.token |
Rejected — package write flows only through the org team |
| Gain package access via repo collaboration | No package access conferred |
| Mint another user's token as admin | Not possible in Forgejo |
| Hostile fork PR against the host-executed workflow | Blocked pending approval; a non-allowlisted collaborator's run skipped by the actor gate |
All clean. When any of the open follow-ups above changes (guard-ppt on app repos), this page changes with it.
For everyone
Experience
Operate
Under the hood
Grow
This wiki ships inside plat/mitosis (wiki/) — edit it there, not here. Grown by the platform it describes.