API keys
Machine credentials, what they may hold, and what they may not.
View as MarkdownAn API key is a machine credential scoped to one workspace, carrying a subset of the permission catalog that an admin chooses when it is created.
It is not a user. It has no session, no MFA and no email, which is why what it is allowed to hold is deliberately narrower than what a person can hold.
Creating one
sctl apikey create ci-deploy --permission backups:read --permission backups:writeCreated API key "ci-deploy" (7c1e...).
Secret (copy now, not shown again):
sk_live_...The secret is shown once and is never stored by us. We keep the key's identifier so we know which key is calling; the secret itself lives only where you put it. If you lose it, create a new key and revoke the old one.
Grant the narrowest set that does the job. A key with backups:read cannot read your data;
a key with artifacts:download can read all of it.
What a key may not hold
Some permissions are refused on any machine credential, whatever you ask for.
| Refused | Reason |
|---|---|
workers:* | A compromised key could mint worker credentials |
api-keys:* | One leaked key could mint more, and revoking the original would not revoke what it made |
billing:* | The workspace's cards |
destinations:write | Linking a bucket hands over a long-lived S3 key pair |
connections:write | The OAuth flow ends at a browser consent screen a machine cannot complete |
artifacts:confirm | Worker-only. Confirming an upload is a worker's signal about its own work |
Asking for one at creation fails with 422. A key that somehow acquired one anyway, minted
before the gate existed or edited directly in the identity provider, has it stripped on
every validation, and the strip is logged as a warning naming the key.
If you see that warning in your own review, the key is over-granted in the identity provider and wants revoking. The stripping means it cannot use the permission, but the record of the grant is still wrong.
Storing a key
The key is a password. Everything that applies to passwords applies here.
| Do | Do not |
|---|---|
| A CI secret store: GitHub Actions secrets, GitLab CI variables | Commit it to a repository |
| A secrets manager: Vault, AWS Secrets Manager, 1Password | Put it in a Dockerfile or an image layer |
| An environment variable read at runtime | Paste it into a ticket, a chat, or a support email |
| A file with restrictive permissions | Log it, or echo it in CI output |
Setting it as an environment variable, per platform:
# Linux and macOS
export SAVED_API_KEY='sk_live_...'# Windows PowerShell, current session
$env:SAVED_API_KEY = 'sk_live_...'
# Windows PowerShell, persisted for the user
[Environment]::SetEnvironmentVariable('SAVED_API_KEY', 'sk_live_...', 'User'):: Windows cmd.exe
set SAVED_API_KEY=sk_live_...On Linux and macOS, a leading space before export keeps the line out of shell history in
most shells. Better still, read the key from a secrets manager rather than typing it.
Using a key
curl -fsS https://api.saved.sh/v1/backups \
-H "Authorization: Bearer $SAVED_API_KEY"The same credential works with the CLI for non-interactive use, which is the usual shape in CI. See CI/CD.
Rotation
Rotate on a schedule, and immediately whenever someone with access to the key leaves or the key may have been exposed.
The zero-downtime order:
- Create the new key with the same permissions.
- Deploy it everywhere the old one is used.
- Confirm traffic is working on the new key.
- Revoke the old key.
sctl apikey list
sctl apikey revoke <key-id>Revocation is not instantaneous. Validations are cached for about a minute, so a revoked key can keep working briefly. Treat revocation as "effective within a minute". If a key leaked, also rotate anything it could reach: source credentials for backups it could reconfigure, and destination credentials if it could read them.
Keys outlive people
A key belongs to the workspace, not to the person who created it. Removing a member does not revoke the keys they made.
That is deliberate, so automation does not break when someone leaves. It also means offboarding has a second step that is easy to forget:
sctl apikey listReview the list, and revoke anything whose owner or purpose you cannot account for. Name keys
after their use rather than their creator (ci-deploy, monitoring, not sams-key), so
that review is possible at all.
Auditing
Key lifecycle is on the audit trail, and the granted permissions are recorded with the creation.
| Action | Recorded | Never recorded |
|---|---|---|
api_key.created | The key ID, name, and the permission slugs granted | The secret |
api_key.revoked | The key ID and name |
Actions taken by a key carry actor.type: machine, identified by the key's ID, so you can
distinguish "an admin deleted this" from "a key deleted this" without reading names. See
Audit log.
API keys versus worker keys
They are the same kind of credential with different rules, and they are not interchangeable.
| API key | Worker key | |
|---|---|---|
| Created by | sctl apikey create | sctl worker provision |
| Permissions | An admin-chosen subset | Exactly artifacts:upload, artifacts:confirm |
May hold artifacts:confirm | No | Yes, and must |
| Also authenticates to the orchestration hub | No | Yes |
| Rotating | Create new, revoke old | sctl worker rotate, keeping the worker ID |
Do not put an API key in a worker's config.yaml. It will authenticate to our API and then
fail to be admitted by the hub, because it lacks the permission the hub requires.