Security
The tenancy and credential model, written for a security reviewer: where data lives, who holds which credential, how trust is derived and what Ciresk never holds.
Topology in three lines
An organization is pinned to exactly one regional cell by its jurisdiction; the only cell today is eu1 in AWS eu-central-1. Everything with content that Ciresk CI stores (source deltas, logs, artifacts, caches, test results) stays in the cell, also when your jobs run on your own workers. Identity (WorkOS), API key records and routing are the shared Ciresk control plane and hold no build content.
| Crosses regions | Never crosses |
|---|---|
| Identity, organization placement, API key records, monthly usage totals | Source, diffs, logs, artifacts, caches, test results, worker metadata with paths |
Tenancy in the cell
| Control | Design |
|---|---|
| Database | Every table with organization data carries the organization id and the region, with row-level security enabled; requests run under a role without BYPASSRLS and without DELETE outside a few short-lived tables (leases, idempotency keys, webhook deliveries, worker credentials); every request transaction opens with the organization set, so a query without it fails rather than leaks. |
| Object store | Keys are tenants/{organization}/... (for example tenants/{organization}/cas/{sha256}); the bucket is encrypted with one customer-managed KMS key per cell; in the cell's AWS account only the server's role is granted access to tenants/, while the worker and release roles reach release/ only; old object versions expire after 30 days. |
| Presigned URLs | Minted for one object key; an upload URL lives at most 15 minutes and never outlives the lease, a download URL at most 60 minutes. |
| Workers | Belong to exactly one organization and lease only that organization's tasks. |
| Region guard | A credential minted for another region is refused with 421 wrong_region before any SQL. |
| Existence | Not-found and forbidden are the same 404 across organizations. |
Credentials: all short-lived, none shared
| Who | Credential | Obtained | Lifetime |
|---|---|---|---|
| A person in the CLI | Access token | Device flow in ciresk ci auth login, stored in the OS keychain | Refreshed in the keychain |
| A person's MCP client | OAuth token for the CI MCP endpoint | OAuth 2.1 from the client | The same |
| A machine, an agent sandbox | csk_live_eu_... key | platform.ciresk.com under API tokens; introspected by fingerprint, cached 60 s, never stored in the cell | Until revoked (effective within 60 s) |
| GitHub Actions, Azure DevOps, an agent sandbox | Cell token (cix_) from POST /v1/ci/auth/exchange | An OIDC token whose issuer, audience and subject match a trust your organization registered, which names the scopes and the trust tier | 1 hour |
| A worker | Worker token (ciw_) scoped to one worker id and the ci:workers.* scopes | join with a one-time enrolment secret an admin issued | 24 hours, renewed by the worker; revoked on deregister |
| A task | Per-task presigned URLs and pass_env values | Minted by the cell at lease time; pass_env resolved on the worker | The lease |
Scopes are ci:<resource>.<verb> (ci:jobs.run, ci:jobs.run_full, ci:repos.write, ci:repos.admin). The MCP endpoint forwards your token to the same API, so every surface (REST, CLI, MCP) checks the same scope for the same capability.
Trust tiers
Trust is derived server-side from principal kind, ref and your organization's policy, stored on the job and carried in the task's cache token. The client never supplies it.
| Tier | Who | Workspace | Isolation | Cache |
|---|---|---|---|---|
trusted_interactive | Your developers and their local agents, on your own branches | Warm, persistent, on a worker you own | Unprivileged user, cgroup caps, egress proxy | Reads its own tier and trusted_ci; writes its namespace |
trusted_ci | Protected branches, CI systems through OIDC exchange | A fresh clone from the worker's mirror per task | Unprivileged user, cgroup caps, egress proxy | Reads and writes; the only writer other tiers read |
untrusted | Fork PRs, policy-marked refs, unknown principals, agent branches when policy says | A fresh clone that keeps no link to the shared mirror | Its own network namespace whose only exit is the proxy, always in block mode; leased only by workers labelled isolation=vm or disposable | Reads trusted_ci entries; writes to a namespace of its own job |
Cache keys are write-once (a second write with different content is refused and audited), entries record their writer's tier, and reads filter by the reader's allowed set. Because an untrusted job writes only to its own namespace, it cannot write a key a trusted build reads, which rules out the cache-poisoning class of attack by construction. Cache archives are checked before restore: no absolute paths, no .., no link that escapes the root.
Isolation on a worker
| Control | Mechanism |
|---|---|
| User | Tasks run as an unprivileged account (ciresk-run by default), dropped to with runuser; the worker itself runs as root. |
| CPU and memory | systemd-run --scope with CPUQuota, MemoryMax, TasksMax; the whole scope is killed on timeout, cancel or lease loss. |
| Egress | A per-task proxy with your organization's allowlist (default: crates.io, github.com, registry.npmjs.org). Audit mode records every unlisted destination on the job and lets it through; block mode refuses it. For trusted_ci and trusted_interactive the proxy is set through proxy environment variables, so a program that ignores them is not stopped; untrusted runs in its own network namespace, where the proxy is the only exit. |
| Instance metadata | 169.254.169.254 and private address ranges are refused by the proxy; on Ciresk's AWS workers an iptables rule rejects metadata traffic from the task user, IMDSv2 has hop limit 1, and the instance role reaches release/ only, not organization objects. |
| Inbound | None; the health endpoint binds 127.0.0.1:9471 by default. |
| Static hosts | A static host (an office box, a PC under WSL2) that is not labelled disposable leases trusted_ci and trusted_interactive work only. |
Ciresk-run workers
Workers that Ciresk runs follow the same rule as yours: each instance is enrolled into one organization and leases only that organization's tasks. The eu1 pool is a fixed set of EC2 instances that are started on queue depth and stopped when idle, plus burst instances launched past them under an hourly cost ceiling and terminated when idle; while an instance runs it is reused between jobs of the same organization rather than replaced.
Egress
Per-organization allowlist checked by the worker's proxy. Audit mode is the default today: every unlisted host is recorded on the job and shown in the log as a [ciresk] egress audit: line, and the connection goes through. Block mode is set by your organization's policy; the untrusted tier is always block mode.
Audit
Every job records its principal, actor (agent id and session), client (CLI, MCP, API, webhook) and region, and every task the worker that ran it. Job runs and cancellations, policy and budget refusals, leases, policy changes, OIDC exchanges, cache write conflicts, worker enrolment, registration and deregistration, repository creation and deletion and webhook changes write an audit_log row with the principal, actor, client, region and X-Request-Id. One shared function writes the table, and the request role can insert into and read the audit log and the job journal but can neither update nor delete them. Rows are readable through GET /v1/ci/audit with the ci:audit.read scope. Audit export and a configurable retention period are not available yet; rows are kept.
Operator access
Ciresk staff have no product path to an organization's source, logs or artifacts: there is no administrative capability over organization content, and every request is fenced by row-level security. Infrastructure paths exist (the owner database credential, the server's storage role, a shell into the server task); they are not yet behind a break-glass gate, and access through them is not recorded in your organization's audit log.
What Ciresk never holds
| Never held by the control plane or the cell | Where it lives instead |
|---|---|
| Your deploy secrets | The worker's own secrets backend (environment, file:, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault); pass_env values go from there into the task process. |
| Git credentials | Never on a worker or in a task: the worker's mirror fetches without one and writes no credential into its config; a private repository runs from the tree the cli uploads. |
pass_env values | Only their names reach the cell; the worker masks the values in the log before it leaves the worker. |
| Trust chosen by the client | Derived in the cell. |
| An agent's prompts, transcripts or tokens | Not sent; the job carries an actor id and a session id only. |
| Source, logs, artifacts, caches outside your cell | The cell's object store under your organization's prefix, encrypted with the cell's key. |
| Long-lived tokens in agent environments | The CLI keeps a person's token in the OS keychain; sandboxes get one-hour OIDC-exchanged tokens or revocable csk_ keys. |
Identity is hosted by WorkOS, which stores identity data in the US; identity holds no source or build content.
Supply chain and compliance
How releases are signed and what is attested today is in Compliance; where every kind of data lives is in Data residency.