NewDebug failed jobs and retry from any step.

Built-in security at every layer.

Every job passes through the same controls at every layer. Each one is listed below, grouped by the layer it guards.

LOOP04beats
01Changesnapshot
02Verifyreplay
03Fixpause
04Mergepublish
Isolate the job, scope its secrets, sign its record, and verify it.

Controls by layer

L1Request
Job admission01

Checked before secrets, scanned before it ends

Before a job receives secrets or an OIDC token, its machine is checked against the pinned image digest. Artifacts and logs are scanned for exfiltration when the job finishes, and a match is refused.

  • Guest attestation runs off, in log-only mode, or in enforce mode. Enforce fails closed on a digest mismatch.
  • The check happens before any secret or OIDC token reaches the job.
  • At finalize, artifacts and logs are scanned for exfiltration within a 512 MiB budget. A match is refused, not just flagged.
L2Identity
Access control02

Six roles, and credentials that expire

Access runs on six roles: owner, admin, maintainer, developer, viewer and ci-agent. People sign in with a device code; agents sign in with federated OIDC or a signed single-use assertion.

  • An access token lasts at most 15 minutes, whether it belongs to a person or an agent.
  • Owner and admin manage the organization. Maintainer and developer run and change workflows. Viewer reads. ci-agent runs and debugs its own jobs, with no secrets, no OIDC, no publishing and no approvals.
  • An agent's working token is the intersection of the grants behind it, the agent's own ceiling and the scopes it asked for, never wider than any one of the three.
  • Environment approvals, fork approvals, agent administration, policy and secret administration stay human-only. No agent token carries them.
  • There is no single sign-on. Every person and every agent gets its own scoped credential.
Workflow identity03

Short-lived tokens bound to your workflow

A workflow's OIDC token carries the repository's immutable numeric ID and the workflow's action lock. If the workflow's actions no longer match the lock, the token is refused.

  • Workflow tokens are RS256 and live for 300 seconds.
  • Each token is bound to the repository's immutable numeric ID, so a rename or transfer cannot inherit an old trust relationship.
  • A token is also bound to the actions pinned in `.github/workflows/actions.lock`. A workflow whose actions no longer match the lock file does not get a token.
L3Tenancy
Tenancy04

One organization's data cannot be reached from another's

Every request is authorized against the principal making it, person or agent, and the action it asks for. Every object your organization owns sits inside its namespace, and a credential minted for one tenant cannot reach another.

  • Tenants are organizations. Runs, jobs, secrets, caches, artifacts, logs, receipts and webhook deliveries all carry the tenant that owns them.
  • A grant is bound to its tenant at mint time. A token issued inside one organization never authorizes against another. Cross-tenant reads answer 404.
  • Authorization is a single decision point: the calling principal, the action, and the resource it names. Roles are fixed sets of actions, not per-request policy.
  • Per-tenant admission policy is platform-owned. Your organization's settings live in its namespace; ours live above it.
L4Execution
Isolation05

One job, one machine, deleted afterwards

Each job runs in its own isolated, ephemeral environment with its own kernel, built from the official GitHub runner image. No Docker daemon is shared, no home directory survives, and the environment is destroyed when the job ends.

  • A second job on the same hardware gets a fresh environment, not a cleaned-up one. There is no state to inherit and nothing to scrub.
  • Caches are namespaced per repository and per git ref, derived server-side from the job's token. A request body cannot choose a namespace.
  • Fork pull requests read caches and cannot write them. They also never receive stored secrets or an OIDC token.
  • A blob is readable by a namespace only if that namespace uploaded it, an entry it can see references it, or the global tier published it. Physical deduplication never becomes a read.
Runner images06

Every image we build is scanned, itemized and diffable

The runner-image pipeline scans every image we build with Trivy at bake time and on rescan. Each digest carries an SBOM, a package inventory, vulnerability and secret findings, and a preflight checklist, all served next to the image a job actually booted.

  • Base images are pinned by digest and a build fails closed on a mismatch. Images are signed, with a provenance statement naming the workflow and base digest they were built from.
  • Every bake produces an immutable CycloneDX SBOM and a package inventory per digest: name, version, ecosystem, license, and the layer that introduced each package.
  • Trivy reports every finding with the CVE, the package, installed and fixed versions, severity, and the layer that introduced it. Secret findings come from the same pass.
  • Evidence uploads over authenticated HTTP with scoped image credentials, is validated server-side, and lands behind a tenant-scoped API under /api/v1/runners/images. Findings attach to the digest, so two digests can be diffed: packages added, removed or changed, and vulnerabilities fixed or introduced.
  • A preflight checklist of up to 25 checks records the health of each bake; the latest result stays attached to the digest.
Transport07

The machines that run jobs authenticate each other with mutual TLS

Every machine in our data plane authenticates with mutual TLS, using certificates from a service certificate authority with pins and revocation. preloop and its runners use scoped bearer and job tokens instead.

  • Each machine in the data plane presents a certificate issued by a dedicated service certificate authority, not public trust.
  • A join token enrolls a new machine. Certificate pins and revocation close the gap when one leaves.
  • preloop and its runners authenticate with scoped bearer and job tokens, not certificates.
L5Secrets
Secrets08

Encrypted at rest, masked in logs

Secrets resolve across five tiers, from a global default down to a single run, with bindings from a ref glob down to one job in one workflow. A job from a fork pull request never receives a stored secret.

  • Secrets resolve in five tiers: run, environment, repository, organization and global, each narrower tier overriding a broader one with the same name.
  • A binding scopes a secret to ref globs, a workflow path, a job key inside it, or a reusable-workflow caller chain down to nested jobs. Conditions inside one binding all match; across bindings, any one does. A secret with no bindings is unrestricted.
  • Hosted values live in the control database as KMS envelope-encrypted rows: AES-256-GCM ciphertext under a fresh data key wrapped by the secrets CMK, with `(org_id, secret_ref, version)` bound into the AEAD. The database holds metadata and a reference, never a plaintext value.
  • Log masking matches a value in raw form and in base64, hex and percent-encoded form, even when it is split across two writes.
  • Fork pull requests never receive a stored secret, and every secret release is recorded in the audit trail.
Keys09

Signing and secret-encryption keys held in KMS

Signing keys and secret-encryption keys live in AWS KMS as non-exportable keys. preloop asks the service to sign a record or decrypt a secret; the private key never leaves KMS.

  • Run records are signed RS256 by a KMS-held key. Public keys are published at a stable location, so a third party verifies a record without a shared secret or an account with us.
  • Public keys rotate. A new key set is published, and records signed under an older set stay verifiable by pinning it.
  • The hosted secrets store encrypts every value under a KMS-held data key, with the row identity bound into the ciphertext; a database copy yields no value.
  • Every KMS operation is recorded by the key service with a time and a caller, so a key use is attributable.
L6Evidence
Attestation10

A signed record of exactly what ran

Every job attempt produces one canonical record and one signature. You verify it against published keys, and it tells you whether preloop observed the job or was told about it.

  • The record covers the captured source tree, whether it had uncommitted changes, the workflow digest, the prepared image and tool versions, the resolved action refs, and every step with its conclusion.
  • It is signed RS256 by preloop. Public keys are published at a stable well-known location, so a third party verifies a record without asking us for anything.
  • Assurance is explicit. Observed means preloop assembled the record from its own state. Reported means an authenticated caller submitted it. A signature proves who signed a record, not who watched the job run.
  • Receipts are append-only and immutable. A new attempt adds a new record; nothing is edited. A receipt id from another organization answers 404, not 403.
  • Verify a record with `plp ci manifest --verify`.
Audit11

A log of who did what

Every action is recorded with the actor, who it acted on behalf of, the action, the resource, and allow or deny. Support access from preloop staff is logged in your organization too.

  • Every mutating call records who made it, what changed, and allow or deny, in an append-only store.
  • Debug sessions keep their own trail, retained with the session, covering attach, retry and close.
  • Secret releases are recorded against the job that received them, so a leaked value has an owner and a time.
  • Support access from preloop staff is time-boxed, requires a reason and an expiry, and is revocable by your org's owner or admin. It is audited in your organization like any other action.

What a signed record proves

Every job attempt produces one canonical JSON record inside a DSSE envelope, signed RS256. A signature proves who signed a record. It does not prove where the job ran or independently verify every claim inside it. Each record includes an assurance level so a reviewer can interpret it directly:

  • Observed: preloop assembled the record from its own state. It created the machine, dispatched the job, watched the steps and wrote the conclusion.
  • Reported: an authenticated caller submitted the record, and preloop signed what it received. It still names the tree, workflow digest, steps and conclusion, and the signature identifies the key that accepted it.

These assurance levels answer different questions. A reviewer can see which one applies without relying on a footnote.

Records are append-only. A new attempt adds a new record; nothing is edited. Public keys are published at a stable, well-known location, so you verify a record without asking us for anything. A record id from another organization answers 404.

Three other fields provide context for the record.

Source binding. The record includes the captured tree, a dirty flag, the workflow digest and an execution binding: bound, unbound or unknown. Only bound connects the captured tree to the tree that the checkout steps executed. Every redirected checkout step must succeed for that binding. unknown stays unknown.

Commit signature evidence. If the commit was signed, the record includes the format, verification status, trust level, coverage and key fingerprint. preloop verifies this at intake and reports the result as signed evidence. It does not verify it again later or upgrade a signature over a dirty tree; that case is recorded as commit-only coverage.

What's missing. The record lists evidence it could not collect. Missing environment evidence stays missing rather than being inferred. A record that lists what's missing shows exactly which questions it answers.

Where each control is enforced

LayerControlDetail
RequestGuest attestationChecks the machine against its pinned image digest before any secret or OIDC token reaches the job. Enforce mode fails closed on a mismatch.
RequestFork pull requestsReceive no stored secrets and no OIDC token. They can read caches and cannot write them.
IdentityRolesOwner, admin, maintainer, developer, viewer and ci-agent, each with a fixed set of actions.
IdentityWorkflow OIDCBound to the repository's immutable ID and to the actions pinned in actions.lock. A mismatch is refused.
IdentitySupport accessTime-boxed, with a reason and an expiry, revocable by your org's owner or admin, and audited in your organization.
TenancyAuthorizationEvery request is authorized against the calling principal and the action it asks for. A token minted for one tenant can never reach another.
TenancyTenant isolationEach organization's data is scoped to its own namespace end to end. Rows, grants, secrets, caches, runs and receipts carry it, and cross-tenant reads are refused at the boundary.
ExecutionMachine transportEvery machine in the data plane authenticates with mutual TLS. preloop and its runners use scoped bearer and job tokens.
ExecutionExfiltration scanArtifacts and logs are scanned for leaked secrets when the job finishes, inside a 512 MiB budget. A match is refused.
SecretsSecret tiersRun, environment, repository, organization and global, each narrower tier overriding a broader one.
SecretsSecret bindingsScoped to a ref glob, a workflow, a job key, or a caller chain down to nested jobs. Conditions inside one binding all match; across bindings, any one does. A secret with no bindings is unrestricted.
SecretsSecret storageHosted values are KMS envelope-encrypted rows in the control database. The database never holds a plaintext value.
SecretsMaskingMatches a secret value in raw, base64, hex and percent-encoded form, even split across two writes.
EvidenceComplianceSOC 2 Type II is in progress. The controls on this page are the operating model being audited.

Verify the result

A record is a canonical JSON payload in a DSSE envelope, signed RS256. You can verify it without an account.

bashbash
# The record for a run, verified against the published key set
plp ci manifest <run-id> --verify

# A released binary, verified against the repository's provenance
gh attestation verify preloop-ubuntu-24.04-aarch64 --repo preloopdev/preloop

A verification failure names the problem: a bad signature, a key outside the published set, a payload that does not match its envelope, or missing evidence that leaves a claim unsupported. These cases do not report a pass.

Security questions

Q.01Can one customer read another customer's cache?

No. Entries are namespaced by repository and ref from the job's own token. A blob is readable only through an allowed namespace or a published global tier.

Q.02What happens for a fork pull request?

A fork job receives no stored secrets or OIDC token. It reads caches and cannot write them. Artifacts and logs use the same masking rules as any other job.

Q.03Can preloop staff access my organization?

Only with a support grant: time-boxed, with a reason and an expiry, revocable by your org's owner or admin. Every action taken under it is audited in your organization like any other action.

Q.04Is job data used to train models?

No. Job content is used to run jobs and produce records. Cache contents are addressed by digest and are not inspected.

Q.05Where is the threat model?

It is published with the assumptions, scope and exclusions for the system. Start there when reviewing the security boundary.

Q.06How do I report a vulnerability?

Email [email protected]. The platform, runner, workflow parsing and execution paths are in scope.

Review the controls with us.

Bring the requirements from your security review. We can walk through the control, the evidence it produces and its limits.