Security model

Treat every CI job as hostile.

The controller is trusted. Workers are temporary and untrusted. The design is meant to keep a compromised job away from permanent credentials, other installations, and production networks.

TrustedController

GitHub App key
Provider credential
Durable budget

UntrustedWorker

No permanent secret
No inbound access
Hard deadline, idle release

Final stateDeleted

Normal completion
Timeout
Reconciliation

Recommended

Your own dedicated GitHub App

RunnerYard creates a private App owned by your user or organization and installs it only on the selected repositories. GitHub places repository runner scale-set management behind Administration write; organization fleets use the narrower Self-hosted runners write permission. No webhook or OAuth permission is requested.

The manifest flow is protected by random state and a local callback. GitHub returns a one-time private key to the CLI; it is verified and written directly to your controller secret sink without being printed.

Alternative

Bring your own App

Use an existing private App when your organization already manages key issuance and rotation. RunnerYard verifies the app identity, installation owner, repository access, and required permission before storing anything.

Terminal
chmod 600 ./existing-app.pem
npx runneryard auth github import \
  --github https://github.com/acme/widgets \
  --client-id Iv1.example \
  --installation-id 123456 \
  --private-key-file ./existing-app.pem \
  --controller-app acme-ci-controller

Not offered

A shared hosted RunnerYard App

One central App is safe only when a hosted broker retains its private key, authenticates every controller, authorizes each installation, and mints short-lived tokens. Shipping the central private key to customer controllers would let any one of them sign as the App.

RunnerYard does not operate that broker today. The default stays fully self-hosted and operator-owned. A hosted control plane can be added later as a separate, explicit trust model, not hidden inside the CLI.

Worker isolation

No credential inheritance

Controller and workers must live in separate secret scopes. Every worker receives only a one-job JIT configuration, runs with no restart policy, has a maximum lifetime, and releases itself when no job arrives. Do not expose self-hosted runners to untrusted public fork pull requests.

Provider credentials should be limited to a dedicated worker app or project. The worker network must have no route to production, controller internals, or cloud metadata services.

Cost containment

Admission fails closed

Before launch, the controller durably reserves the job's worst-case runtime. New work stays queued when concurrency or rolling usage ceilings are reached. Missing or unreadable budget state stops the controller.

Provider-side spending alerts remain useful because network, image storage, the controller, and provider price changes sit outside the runtime ceiling.

Outboarding

Leave without RunnerYard

  1. Disable the fleet route and confirm jobs target ubuntu-latest.
  2. Stop the controller after active jobs finish; verify zero workers.
  3. Revoke the provider token.
  4. Dedicated App: revoke its keys, uninstall it, and delete it.
  5. Shared BYO App: remove this controller's local credential first. Change repository access only after confirming no remaining consumer needs it. For an organization fleet, retire its scale set and runner-group access as applicable without treating the shared installation as controller-owned.
  6. Keep shared keys, installations, and the App until every remaining consumer rotates away.
  7. Delete provider resources, generated config, local credential files, and the repository variable.

Read the complete threat model