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.
Every job passes through the same controls at every layer. Each one is listed below, grouped by the layer it guards.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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:
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.
| Layer | Control | Detail |
|---|---|---|
| Request | Guest attestation | Checks the machine against its pinned image digest before any secret or OIDC token reaches the job. Enforce mode fails closed on a mismatch. |
| Request | Fork pull requests | Receive no stored secrets and no OIDC token. They can read caches and cannot write them. |
| Identity | Roles | Owner, admin, maintainer, developer, viewer and ci-agent, each with a fixed set of actions. |
| Identity | Workflow OIDC | Bound to the repository's immutable ID and to the actions pinned in actions.lock. A mismatch is refused. |
| Identity | Support access | Time-boxed, with a reason and an expiry, revocable by your org's owner or admin, and audited in your organization. |
| Tenancy | Authorization | Every request is authorized against the calling principal and the action it asks for. A token minted for one tenant can never reach another. |
| Tenancy | Tenant isolation | Each 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. |
| Execution | Machine transport | Every machine in the data plane authenticates with mutual TLS. preloop and its runners use scoped bearer and job tokens. |
| Execution | Exfiltration scan | Artifacts and logs are scanned for leaked secrets when the job finishes, inside a 512 MiB budget. A match is refused. |
| Secrets | Secret tiers | Run, environment, repository, organization and global, each narrower tier overriding a broader one. |
| Secrets | Secret bindings | Scoped 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. |
| Secrets | Secret storage | Hosted values are KMS envelope-encrypted rows in the control database. The database never holds a plaintext value. |
| Secrets | Masking | Matches a secret value in raw, base64, hex and percent-encoded form, even split across two writes. |
| Evidence | Compliance | SOC 2 Type II is in progress. The controls on this page are the operating model being audited. |
A record is a canonical JSON payload in a DSSE envelope, signed RS256. You can verify it without an account.
# 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/preloopA 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.
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.
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.
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.
No. Job content is used to run jobs and produce records. Cache contents are addressed by digest and are not inspected.
It is published with the assumptions, scope and exclusions for the system. Start there when reviewing the security boundary.
Email [email protected]. The platform, runner, workflow parsing and execution paths are in scope.
Bring the requirements from your security review. We can walk through the control, the evidence it produces and its limits.