Naar de hoofdinhoud

Beveiligingsmodel van de gehoste runners

De gehoste runners van fremforge voeren de Forgejo Actions-workflows van klanten uit op virtuele ECS-machines van T Cloud Public, één per commit, in de regio EU-DE. Een job in de wachtrij waarvoor geen vrije VM van zijn commit bestaat, krijgt een nieuwe VM met ingebouwde Docker. Die VM draait alleen jobs van dezelfde commit in dezelfde repository, één tegelijk, wacht maximaal 60 seconden op een volgende job, en wordt dan uitgeschakeld en vernietigd. Er is geen permanente runnerpool, en geen enkele VM draait ooit code van twee commits, twee repository’s of twee klanten.

Deze pagina beschrijft de isolatiegaranties die fremforge geeft, de limieten die per tenant gelden en het dreigingsmodel: wat klantcode kan, wat ze niet kan, en welke ontwerpkeuzes een workflowauteur maakt om zijn secrets veilig te houden.

Waar workflows draaien

OnderdeelImplementatie
RekenkrachtVirtuele machines op T Cloud Public ECS (Elastic Cloud Server), regio eu-de, drie beschikbaarheidszones
LevenscyclusEén kortlevende VM per commit. Het platform start een nieuwe ECS-VM zodra de eerste job van een commit in de wachtrij komt, en vernietigt die zodra de jobs van die commit klaar zijn. Twee klanten, en twee commits, delen nooit een kernel: voor isolatie tussen tenants vergelijkbaar met de gehoste runners van GitHub en GitLab. Jobs van dezelfde commit kunnen de VM delen (bijvoorbeeld build en daarna deploy), en daarom behandel je een pipeline als één vertrouwensdomein; zie Dreigingsmodel. Klantworkflows delen nooit een VM met platformworkloads. Er is geen warme pool (doel 0, bewust vanwege de kosten), dus elke commit betaalt de opstarttijd van ECS van ~40 s.
NetwerkplaatsingElke VM krijgt een eigen ENI in een apart runnersubnet, met een Security Group per VM die het uitgaande verkeer afdwingt. De VM’s staan in een apart VPC-subnet, verdeeld over drie beschikbaarheidszones
Looptijd van de VMEén VM per commit; aangemaakt zodra de eerste job van die commit in de wachtrij komt, vernietigd wanneer de jobs klaar zijn. Een maximale looptijd per job (standaard 60 min, harde bovengrens 360 min) wordt op de runner afgedwongen
OpruimvangnetEen aparte reaper buiten de normale route verwijdert verweesde VM’s: een runner die zich nooit registreerde, een runner die stopte terwijl het verwijderen werd gemist, een VM die zijn opruimdeadline voorbij is, of een absoluut vangnet van 6 u 10 min.
RunnerbinaryUpstream forgejo-runner (act_runner) v13.2.0, geen fork. Registreert zich kortlevend met --ephemeral, zodat een registratie door één taak wordt verbruikt; de agent registreert zich opnieuw voor elke volgende taak van dezelfde commit, en het platform vernietigt de VM zodra de jobs van die commit klaar zijn
VM-imageHet image van de gehoste runner: basis debian 13 (trixie), gebakken met toolchains uit binaries die in de EU zijn gecachet (geen directe downloads van upstream tijdens het opstarten). Het image is de opstartschijf van de VM; de Docker-daemon is vooraf geïnstalleerd en draait direct op de VM (geen aparte sidecar). Een koude start duurt bij elke commit ~40 s: het doel voor de warme pool is 0, bewust vanwege de kosten, dus er staat geen VM vooraf klaar

Isolatiegaranties

Netwerkisolatie (per VM)

Elke runner-VM krijgt een eigen ENI, gekoppeld aan een Security Group per VM. De SG weigert standaard al het verkeer, behalve:

  • DNS (53/UDP, 53/TCP) naar de interne DNS-resolver van het platform
  • Uitgaand verkeer via de egressproxy voor runners: de enige uitgaande HTTP-route

Klantcode kan niet bij:

  • De fremforge-API of de bijbehorende privédatabase
  • Het interne endpoint van Forgejo (runner-VM’s praten met Forgejo via het publieke apexdomein achter Bunny, met een scope die specifiek is voor runners; het interne endpoint is onbereikbaar)
  • Platformdiensten van T Cloud Public (RDS, DCS, OBS, SFS-Turbo, IAM, KMS)
  • Runner-VM’s van andere tenants
  • Het metadata-IP van het cluster (169.254.169.254), dat in de SSRF-guard expliciet wordt geblokkeerd
  • Private RFC1918-bereiken of loopback

De Security Group wordt bij het aanmaken van de VM toegepast en kan vanuit de VM niet worden gewijzigd: aan de VM hangt geen IAM-agency, dus de API’s voor netwerkconfiguratie zijn via de identiteit uit de metadata onbereikbaar.

Uitgaand verkeer

Workflowstappen die het publieke internet nodig hebben (een publieke repo klonen, een tool downloaden, een SaaS-API aanroepen) gaan via de egressproxy voor runners, via een HTTPS_PROXY die in de VM wordt geïnjecteerd. Deze proxy:

  • Accepteert elk publiek unicast-IP, inclusief Carrier-Grade NAT (net als de gehoste runners van GitHub; nodig voor diensten van T Cloud Public zelf, zoals SWR, die in CGNAT zitten)
  • Blokkeert private RFC1918-adressen, link-local, loopback, multicast, gereserveerde adressen en expliciete cloud-metadata-IP’s (169.254.169.254, 100.100.100.200 van T Cloud Public)
  • Logt elke poging tot een CONNECT-tunnel met hostnaam en opgelost IP, zodat die voor audit bewaard blijft

De proxy is bewust ruim in de zin van de gehoste runners van GitHub: klantworkflows bereiken elke publieke host die ze nodig hebben. De grens ligt bij SSRF en het interne platform, niet bij allowlists van leveranciers.

Pakketcaches (soeverein, in de EU)

Voordat een workflow het publieke internet bereikt, gaan pakketinstallaties en image-pulls via de eigen dependency proxy van je organisatie, die in de EU staat, in dezelfde regio als de runner-VM’s. De gedeelde Docker Hub-mirror en de pakketcaches binnen de VPC zijn in oktober 2026 uitgefaseerd. De proxy van elke organisatie past een eigen pakketbeleid, eigen upstream-inloggegevens, een eigen toegangslog en een eigen opslaglimiet toe; een publiek bestand dat meerdere organisaties ophalen, wordt één keer opgeslagen, en inhoud die met de eigen inloggegevens van een organisatie is opgehaald, wordt nooit aan een andere organisatie geleverd. Met runner pull-through aan (de standaard voor elke organisatie die geen eigen instellingen voor de dependency proxy heeft opgeslagen) wordt elke job bij de start naar de proxy verwezen:

VerkeerGeïnjecteerd als
npm, pnpm, pip, uvde npmrc van de job, PIP_INDEX_URL, UV_DEFAULT_INDEX
Go, Maven, NuGet, Trivy-kwetsbaarheidsdatabase (elk zoals vermeld in runner_pull_through_ecosystems, standaard allemaal)GOPROXY, een Maven-mirror van Central, een NuGet.Config voor de hele machine, TRIVY_DB_REPOSITORY en TRIVY_JAVA_DB_REPOSITORY
Containerimages van ghcr.io, quay.io, mcr.microsoft.com, registry.k8s.io, public.ecr.aws, registry.access.redhat.com; en docker.io, registry.redhat.io, registry.connect.redhat.com alleen met de eigen inloggegevens van de organisatie, nooit voor pull requests uit forksde registryconfiguratie van de Docker-daemon op de VM

De job authenticeert bij de proxy met inloggegevens die beperkt zijn tot zijn eigen organisatie en run, zodat pakketinstallaties door het pakketbeleid en de malwarecontroles van de organisatie gaan en in haar toegangslog verschijnen. Voor containerimages op deze route is de proxy een cache, geen poort: een pull die de proxy weigert, valt terug op de registry. De proxy wordt aan het begin van elke job gecontroleerd; is hij niet bereikbaar, dan installeert en pullt de job via de egressproxy van de publieke registry’s, en wordt die terugval gelogd. Zie Gehoste runners.

In het gangbare geval (publieke afhankelijkheden) merk je hier niets van, dus in je workflow verandert niets.

Gebruik je een private index of registry, let dan op: de geïnjecteerde waarde is de standaard, dus die gaat vóór een standaard uit een gecommit configuratiebestand (pip.conf, .npmrc, go env). Verwijs expliciet naar je private bron, zodat die wint; hoe je dat per tool doet, staat hieronder.

Gebruik één index per installatie: de proxy, of één private index die zelf de publieke registry spiegelt. Voeg nooit een tweede index naast de eerste toe (--extra-index-url, PIP_EXTRA_INDEX_URL, UV_EXTRA_INDEX_URL): de installer neemt een pakketnaam dan van de index die hem aanbiedt, en zo werken dependency-confusion-aanvallen. Zie de PyPI-opmerkingen bij de dependency proxy.

  • pip: pip install --index-url <your-private> ... (een --index-url op de opdrachtregel gaat vóór de geïnjecteerde PIP_INDEX_URL), of stel hem in onder env: van de workflowstap, als die private index de publieke pakketten spiegelt die je nodig hebt. Wil je je eigen pakketten uit de registry van je organisatie installeren terwijl publieke pakketten via de proxy komen, gebruik dan twee aparte installaties, elk met één eigen index: eerst de publieke afhankelijkheden, daarna je eigen pakketten met --index-url https://frem.sh/api/packages/<org>/pypi/simple/ --no-deps.
  • uv: dezelfde regel. Pin je eigen pakketten op naam aan je registry, met een index die als explicit = true is gemarkeerd en een [tool.uv.sources]-regel per pakket, zodat geen enkele naam uit beide kan worden opgelost.
  • npm: stel NPM_CONFIG_REGISTRY (en voor pnpm 11 ook PNPM_CONFIG_REGISTRY) in onder env: van de stap, naar één registry die de publieke spiegelt, of houd de proxy en stuur alleen je scope naar je registry (@<your-scope>:registry=…), waarmee elke naam in die scope aan één registry wordt vastgepind. Zie Pakketcaches.
  • Go: stel GOPRIVATE in (bijv. GOPRIVATE=git.example.com/*), zodat de toolchain de proxy en de checksumdatabase overslaat voor je private modulepaden.

Isolatie op VM-niveau

OppervlakAfdwinging
ProcesWorkflowstappen draaien standaard als de gebruiker runner van de VM; sudo is beschikbaar, zodat workflows apt install kunnen doen, docker kunnen draaien, systeemtools kunnen configureren enzovoort (zoals je van de gehoste runners van GitHub verwacht). De grens is de hypervisor onder de VM, niet user namespaces
WerkruimteLokale werkruimte op de VM in /workspace (en /tmp): gewist tussen jobs en vernietigd met de VM. Zie Jobs die een VM delen voor wat tussen jobs van één commit kan overblijven
IdentiteitAan de VM hangt geen IAM-agency. Vragen aan de metadataservice geven alleen de netwerkidentiteit terug die nodig is om op te starten; geen toegang tot de platform-API
ResourcegrenzenHet ECS-flavor van de VM legt een harde bovengrens voor CPU en geheugen vast. Een op hol geslagen klant kan niet meer capaciteit opgebruiken dan de grenzen van zijn eigen VM. Op hol geslagen gebruik over het hele platform wordt begrensd door de limiet voor gelijktijdige jobs per tenant en de rekencapaciteit van het platform
ContainerruntimeDocker (native): de daemon draait op de VM-host, niet als sidecar. docker build, docker compose, kind en testcontainers werken direct, zonder de complexiteit van DinD. Voor workflows die extra mogelijkheden van de containerruntime nodig hebben, stuurt het label dind-privileged de job naar een flavor met KVM-nesting
HypervisorDe KVM-gebaseerde ECS-hypervisor van T Cloud Public: dezelfde VM-grens tussen tenants die geldt voor elke commerciële klant van T Cloud Public die in de regio ECS draait

Kernelisolatie tussen en binnen tenants

Dit is de dragende bewering van het model van de gehoste runners. Elke VM hoort bij één commit van één repository: een Linux-gastkernel op de hypervisor van T Cloud Public. Hij draait de jobs van die commit één voor één, en het platform vernietigt hem zodra ze klaar zijn. Een job van een andere commit, een andere repository of een andere organisatie krijgt altijd een andere VM met een andere kernel.

MechanismeHoe
VM per commitHet platform start een VM voor een wachtende job als er geen vrije VM van die commit is. Een ontsnapping op kernelniveau (bijv. CVE-2024-1086 nf_tables) compromitteert alleen DIE wegwerp-VM en de jobs van dezelfde commit, niet tenants op dezelfde host
Snel afbreken na afloopNa zijn laatste job wacht de VM maximaal 60 seconden op een volgende job van dezelfde commit, en wordt dan uitgeschakeld en verwijderd. Een aparte reaper buiten de normale route vangt verweesde VM’s op (runner nooit geregistreerd, verwijderen gemist) binnen een absoluut vangnet van 6 u 10 min
ENI en SG per VMNetwerkisolatie die losstaat van de kernelgrens. De VM van tenant A kan de VM van tenant B niet over het netwerk bereiken: aparte ENI’s, aparte Security Groups
Levenscyclus van één commitElke job draait in een nieuwe jobcontainer. Een VM wordt nooit hergebruikt voor een andere commit, repository of tenant, dus hij bevat nooit de secrets van een vorige tenant
OIDC-tokensElke VM krijgt een nieuw OIDC-token, ondertekend door de runner-OIDC-issuer, met aud=fremforge en sub=repo:<owner>/<repo>:ref:<refname>. Trust policies in de cloud van de klant sleutelen voor assume_role_with_web_identity op deze audience en sub

Jobs die een VM delen

Een VM wordt alleen hergebruikt voor jobs van dezelfde commit in dezelfde repository (en dus dezelfde organisatie): andere workflows, runs of nieuwe runs van die commit, of een deploy na de build ervan. Tussen die jobs zet fremforge alles terug waarvan bekend is dat het een inloggegeven kan bevatten: de jobcontainer, /workspace en /tmp, de Docker-login en de npm-configuratie, en de gebruikelijke bestanden met inloggegevens. Kan die reset niet worden bevestigd, dan neemt de VM geen job meer aan en krijgt de volgende job een nieuwe VM.

De reset is geen nieuwe VM. Jobs kunnen de Docker-daemon van de VM gebruiken, die root is op de VM, dus een job kan iets achterlaten voor een latere job van dezelfde commit. Images die met inloggegevens zijn gepulld, de toolcache en de Go-buildcache blijven ook op de VM staan. Behandel alle jobs van één commit als één vertrouwensdomein:

  • Draai geen onvertrouwde code (fixtures van bijdragers, niet-gereviewde updates van afhankelijkheden) in de pipeline van dezelfde commit als een job die productiesecrets heeft.
  • Zet deploys achter een goedkeuring van een omgeving (zie Splits jobs voor validate en apply), en kies liever kortlevende OIDC-inloggegevens dan opgeslagen sleutels.
  • Mag een deploy nooit een machine delen met een build, draai hem dan op een eigen runner (BYO).

Limieten per tenant

LimietStandaardOverschrijven
Gelijktijdige jobs per tenantGegarandeerd 2 per gebruiker, minimaal 10 en maximaal 60 (proefperiode: 2). Daarboven mag een betalende tenant vrije capaciteit gebruiken tot 60, maar alleen zolang er 20 platformslots vrij blijven, zodat de piek van de ene tenant de gegarandeerde jobs van een andere niet vertraagt. Ter vergelijking, gehost bij GitHub: Free 20, Pro 40, Team 60, Enterprise 500Enterprise-on-Demand: een overschrijving per tenant vervangt het gegarandeerde aandeel
Gelijktijdige jobs over alle tenantsBegrensd door de rekencapaciteit van het platform in eu-de—
Limiet op jobstarts7 jobstarts per voortschrijdend venster van 5 minuten per slot van het piekplafond van de tenant: 420 voor een betalende tenant, 14 voor een proefperiode. Hij schaalt mee met een verhoogde limiet voor gelijktijdigheid, dus het is een vangnet tegen misbruik en nooit de limiet die een normale pipeline als eerste raakt. GitHub publiceert geen limiet op jobstartsOverschrijving per tenant
Looptijd per job3600 s (60 min)Per workflowstap in te stellen, maar met een plafond van 6 u
Pool van runnerminutenseat_cap × 1000 minutes/month (of een expliciete limiet per tenant)Configuratie door de operator; komt overeen met de publieke prijzen
VM-flavorStandaard c9.large.2 (2 vCPU / 4 GiB RAM); c9.xlarge.4 (4 vCPU / 16 GiB) voor runs-on: [fremforge, large]Het label runs-on: van een workflowstap stuurt naar een andere flavor

Overschrijdt een tenant de pool van runnerminuten en is overschrijding niet ingeschakeld (of bevroren), dan weigert het platform het toewijzen: de job in de wachtrij gaat in Forgejo naar Cancelled. Tenants met ingeschakelde overschrijding blijven draaien en krijgen het meerverbruik gefactureerd.

Bereikt een tenant de limiet voor gelijktijdige jobs, dan wachten de extra jobs in de wachtrij en worden ze toegewezen zodra er een slot vrijkomt.

Wat klantcode NIET kan

  • De fremforge-API, het interne endpoint van Forgejo of een andere platformdienst bereiken (de API is alleen beschikbaar via het publieke Bunny-apexdomein met de juiste authenticatie, net als voor elke externe aanroeper)
  • De runner-VM van een andere tenant lezen (aparte ENI; verkeer tussen VM’s expliciet geweigerd in de Security Group)
  • De secrets van een andere tenant lezen (workflowsecrets gaan alleen naar de runner die de workflow draait waar ze bij horen; het vernietigen van de VM wist de inloggegevens in het geheugen)
  • Status bewaren voorbij de eigen commit (de VM wordt vernietigd zodra de jobs van die commit klaar zijn, en er wordt geen schijf tussen VM’s gedeeld)
  • De OTC-API rechtstreeks bereiken met bevoorrechte scope (geen IAM-agency gekoppeld; de identiteit uit de metadata heeft geen rollen)
  • Cloud-metadata-endpoints bereiken (169.254.169.254, 100.100.100.200: beide expliciet geweigerd in de SSRF-guard)
  • De runnerpool uitputten (limiet voor gelijktijdige jobs per tenant plus het globale rekenplafond van het platform)
  • Wachtrijen van andere tenants laten verhongeren (limiet voor gelijktijdige jobs per tenant)
  • Uit de hypervisor ontsnappen (de VM is de grens; de host eronder is van T Cloud Public, geïsoleerd door dezelfde VMM die elke commerciële ECS-tenant in de regio scheidt)

Aanbevolen vormen voor workflows

Splits jobs voor validate en apply

Draai onvertrouwde code (PR-builds, scans van afhankelijkheden, testfixtures van bijdragers) in een validate-job zonder productie-inloggegevens, en zet de apply-job achter een goedkeuring van een omgeving. De validate-job ziet de secrets van de apply-job nooit. Beide jobs horen wel bij dezelfde commit, dus ze kunnen na elkaar op dezelfde VM draaien: zie Jobs die een VM delen.

Pin uses:-verwijzingen op SHA als de supply chain ertoe doet

uses: actions/checkout@v4 verwijst naar frem.sh/actions/checkout (de native Forgejo-pull-mirror van fremforge voor github.com/actions/checkout, gesynchroniseerd elke 8 uur). De mirror volgt upstream-tags, inclusief force-pushes, dus pinnen op SHA is de enige manier om byte-voor-byte stabiliteit te garanderen:

# Bytewise-stable across maintainer force-pushes
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.2.0

# Picks up upstream's current HEAD at last mirror sync — including
# any maintainer-credential-compromise force-push
- uses: actions/checkout@v4

Gebruik OIDC voor cloudauthenticatie, geen langlevende secrets

Elke runner-VM krijgt een nieuw OIDC-token met aud=fremforge. Stel de trust policy van je cloudprovider zo in dat die dit token accepteert en inruilt voor kortlevende inloggegevens. Sla geen langlevende AWS-toegangssleutels of JSON-bestanden van GCP-serviceaccounts op in workflowsecrets, waar ze in de procesomgeving terechtkomen.

Begrens de deadlines van workflowstappen

Stel timeout-minutes: in op afzonderlijke stappen om te begrenzen hoe lang een vastgelopen stap kan draaien. De maximale looptijd op VM-niveau (activeDeadlineSeconds=3600) is de ondergrens van het platform; time-outs per stap geven sneller feedback.

Samenvatting van het dreigingsmodel

DreigingMaatregel
Een klantworkflow bevat een inloggegevenPush-bescherming (Gitleaks) blokkeert de commit voordat die in de geschiedenis belandt
Een klantworkflow probeert het fremforge-platform te lezenENI en SG per VM weigeren; geen IAM-agency aan de VM; de API is alleen bereikbaar via de publieke route met authenticatie
De job van de ene tenant probeert de VM van een andere te lezenENI per VM op aparte IP’s; SG weigert standaard alles binnen het runnersubnet; en ze staan op verschillende VM’s, met verschillende kernels
Ontsnapping op kernelniveau (root in de VM → hostkernel via een CVE)Compromitteert alleen de wegwerp-VM die de jobs van die commit draait. Tenants op dezelfde host NIET: die staan op hun eigen VM. De VM wordt hoe dan ook ongeveer een minuut na de laatste job van die commit vernietigd
Een klantworkflow probeert gegevens via DNS weg te sluizenDNS is toegestaan (workflows hebben het terecht nodig), maar de egressproxy logt elke CONNECT met de volledige hostnaam voor audit
Gecompromitteerde upstream-act_runnerBinary die upstream is getest (we forken niet). Gespiegeld via de EU-cache, met Trivy-poorten op het runner-image. CVE-fixes komen via de wekelijkse rebuild binnen
Een klantworkflow probeert metadata-IP’s te bereikenExpliciete weigering in de SSRF-guard: 169.254.169.254 (AWS-stijl) en 100.100.100.200 (T Cloud Public)
Een klant legt beslag op alle capaciteitGegarandeerd aandeel per tenant (2 per gebruiker, 10 tot 60) met pieken alleen in vrije capaciteit die 20 slots vrij laat, limiet op jobstarts (7 starts per piekslot per 5 min), limiet op poolminuten
Een verouderde runner-VM overleeft zijn taakDe maximale looptijd per job stopt hem hard bij de deadline; het platform verwijdert de VM zodra de runner stopt; een aparte reaper buiten de normale route vangt verweesde VM’s op met een absoluut vangnet van 6 u 10 min

Controleerbaarheid

Elke toewijzing wordt op drie plekken gelogd:

  1. Tabel runner_jobs: tenant_id, repo, forgejo_task_id, started_at, finished_at, duration_seconds, vm_id. Bron voor de facturatie-aggregatie en het tabblad Infrastructuur in de operatorconsole
  2. Tabel metering_events: rijen met code=runner_minute, afgeleid van runner_jobs.duration_seconds. Voedt de factuurregels en het paneel “Runner minutes used” van de tenant
  3. Logstroom van de operator: gestructureerde startlogs voor de operator, met doorlopende alerting op geweigerde toewijzingen, vastgelopen VM’s, overschreden deadlines en uitgeputte capaciteit

Tenants zien hun eigen gebruik onder Organisatiebeheer → Bouwen en uitrollen → CI-gebruik, met een minutentelling per maand en een tabel met de duur per job.