1 Security Model
mitosis edited this page 2026-07-29 08:11:35 -05:00

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:2375docker run --privileged -v /:/host → node root) is documented in the gitops repo as the reason. A runs-on: docker job 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-env endpoints — 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 address 169.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 use pull_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_request run 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 ** carries protected_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.token in a workflow is Forgejo's ephemeral per-run token (synthetic user forgejo-actions): 200 on its own repo, 404 on every other repo — same-org included. Hard-tested.
  • REGISTRY_TOKEN belongs to registry-bot-<org>: a restricted user in a packages-only team (registry-writers, units_map: {"repo.packages": "write"}), holding a write: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-bot carries write:repository,write:issue only. 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.