[security] Non-admin lease creation can choose provider key names later treated as Crabbox-owned cleanup targets
Summary
The coordinator's POST /v1/leases route accepts providerKey from any
authenticated caller and stores it as the lease's provider key without an
admin gate, per-owner namespace, or validation against the lease id. The
normal Go CLI derives this value from the lease id before sending the request
(internal/cli/provider_coordinator.go:41-59,
internal/cli/lease.go:155-160), and the documentation describes brokered
provider keys as stable per-lease names (docs/features/ssh-keys.md:51-62).
The HTTP API itself does not enforce that invariant.
That makes providerKey a non-admin-controlled provider-resource identifier.
When it matches Crabbox's cleanup pattern (crabbox-cbx-[a-f0-9]{12}), the
AWS and Hetzner release paths later treat the name as a Crabbox-managed key
and delete it under the broker's provider credentials. This crosses the
maintainer-documented boundary for destructive provider actions against
resources Crabbox cannot strongly identify as its own: a low-privilege
coordinator user can cause provider key-pair or SSH-key deletion using a name
that was not derived from their lease.
The original draft overstated two details that are not supported by current
source. First, cleanup is not triggered for arbitrary names such as the legacy
default crabbox-steipete; both AWS and Hetzner cleanup require the
crabbox-cbx-<12 hex> pattern. Second, deleting a provider key object does not
by itself prove loss of SSH access to an already-running machine, because cloud
providers usually inject the public key at instance creation time. The
source-backed impact is unauthorized mutation/deletion of provider-side
Crabbox key records and identity confusion in broker-managed provider
metadata.
Affected Components
- Checked commit:
6dba4afd1c5a4be0a780cf035d1b397e8982d478
- Component: coordinator lease creation, provider SSH key identity, provider
cleanup.
- Affected files and lines:
worker/src/types.ts:124-194 includes providerKey in the public
LeaseRequest body.
worker/src/fleet.ts:867-872 exposes POST /v1/leases as a non-admin
route.
worker/src/config.ts:112-120 and worker/src/config.ts:255-327 build a
LeaseConfig directly from the request body; providerKey is assigned
from input.providerKey ?? "crabbox-steipete" at config.ts:316.
worker/src/fleet.ts:1645-1671 reserves only the workspace provider-key
prefix and admin-gates native image sources and host pinning. It does not
reject or derive caller-supplied ordinary providerKey values.
worker/src/fleet.ts:1737-1758 stores the chosen config.providerKey in
the lease record.
worker/src/fleet.ts:12771-12773 defines the cleanup-trusted key pattern
as crabbox-cbx-[a-f0-9]{12}.
worker/src/aws.ts:1018-1035 accepts an existing EC2 key pair by name
without verifying that its public key matches the lease's submitted public
key, and worker/src/aws.ts:1067-1072 uses that same name as the EC2
KeyName.
worker/src/fleet.ts:15607-15608 deletes an AWS EC2 key pair on release
whenever the stored lease providerKey matches the Crabbox cleanup
pattern.
worker/src/hetzner.ts:97-151 uses providerKey as the Hetzner SSH-key
name for lookup/create/delete. Hetzner does compare public-key identity
during creation (hetzner.ts:103-108), which reduces the collision impact,
but cleanup still deletes by raw matching name when the stored key passes
the coordinator cleanup predicate (worker/src/fleet.ts:14949-14953).
worker/src/provider-labels.ts:4-23 stores the caller-selected provider
key in provider labels, so provider metadata no longer reliably reflects a
broker-derived per-lease identity.
Attack Path
Attacker role: authenticated non-admin coordinator user with a signed user
token or shared operator token.
Prerequisites:
- The deployment exposes the coordinator lease API to authenticated users.
- The broker is configured for AWS or Hetzner.
- The attacker can send a crafted
POST /v1/leases request rather than using
the stock Go CLI, which normally derives ProviderKey from the new lease id.
- For the strongest AWS path, the attacker chooses a
providerKey matching the
Crabbox cleanup pattern and colliding with a provider key pair that already
exists in the broker's AWS account.
Steps:
- Send
POST /v1/leases as a non-admin caller with a body containing a
chosen providerKey, for example a crabbox-cbx-<12 hex> value associated
with another lease or a guessed Crabbox-managed provider key.
leaseConfig copies the value into config.providerKey
(worker/src/config.ts:316), and createLease stores it in the lease record
(worker/src/fleet.ts:1737-1758). The ordinary non-admin guard at
worker/src/fleet.ts:1645-1671 does not reject the value unless it uses the
reserved workspace prefix.
- On AWS,
ensureSSHKey calls DescribeKeyPairs and returns successfully if
the chosen key name already exists (worker/src/aws.ts:1018-1021), without
comparing the submitted public key to the existing EC2 key pair. The
instance launch then uses the attacker-selected KeyName
(worker/src/aws.ts:1067-1072).
- When the attacker's lease is released and has a cloud instance to clean up,
AWS cleanup calls DeleteKeyPair for the stored lease.providerKey if it
matches crabbox-cbx-[a-f0-9]{12} (worker/src/fleet.ts:15607-15608,
worker/src/aws.ts:1003-1009).
- On Hetzner, a mismatched existing key blocks server creation
(worker/src/hetzner.ts:103-108), so the same collision is harder to turn
into a successful active lease. However, the API still accepts the
non-admin-selected provider key as the provider SSH-key name, and successful
leases with cleanup-pattern names are later deleted by raw name
(worker/src/fleet.ts:14949-14953, worker/src/hetzner.ts:142-151).
Control/data flow:
authenticated non-admin request body
-> LeaseRequest.providerKey
-> leaseConfig(...).providerKey
-> stored LeaseRecord.providerKey
-> AWS KeyName / Hetzner SSH key name and provider labels
-> release cleanup deletes matching crabbox-cbx-* provider key
Impact
A non-admin coordinator caller can make the broker perform provider key
operations against an identifier that is not derived from the caller's lease.
For AWS, the source proves that an existing EC2 key pair name is accepted
without public-key verification, used for instance launch, and then deleted on
release if it matches Crabbox's cleanup pattern. For Hetzner, public-key
identity comparison prevents the direct mismatched-key creation path, but the
same unvalidated identifier still reaches the provider key namespace and
cleanup code.
The practical impact is provider-resource integrity and availability damage:
Crabbox can delete or mislabel provider-side SSH key resources that it cannot
strongly bind to the releasing lease. This matches the in-scope policy example
for destructive provider actions against resources Crabbox cannot strongly
identify as its own. The issue does not expose private SSH keys, and current
source does not prove that deleting the provider key object removes SSH access
from already-running instances, so the severity should stay below the original
draft's high-impact availability claim.
Severity Assessment
CVSS Assessment
| Metric |
v3.1 |
v4.0 |
| Score |
5.4 / 10.0 |
5.3 / 10.0 |
| Severity |
Medium |
Medium |
| Vector |
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L |
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Calculator |
CVSS v3.1 Calculator |
CVSS v4.0 Calculator |
The score reflects a network-reachable authenticated non-admin caller, no user
interaction, no confidentiality impact, and limited integrity/availability
impact to provider-side key-pair identity and lifecycle consistency. The
score is not higher because current source does not prove private-key exposure,
admin-token compromise, cross-owner coordinator data access, or guaranteed SSH
loss on already-running machines.
Recommended Remediation
- Do not trust
providerKey from ordinary POST /v1/leases requests. For
non-admin requests, derive the provider key server-side from the final lease
id, matching the documented providerKeyForLease format
(crabbox-<lease id> with underscores rewritten to dashes).
- Preserve a narrowly reviewed admin-only or internal path only if operators
still need to supply a provider key manually. That path should reject
crabbox-cbx-* names that do not correspond to the lease being created, or
otherwise record ownership metadata that cleanup can verify before deletion.
- For AWS, verify that an existing EC2 key pair name matches the submitted
public key before treating it as reusable. If AWS cannot return enough key
material to prove a match, fail closed or require an admin-only override.
- For Hetzner and AWS cleanup, delete provider key resources only when the key
name is derived from the same lease id or when stored ownership metadata shows
Crabbox created that exact key for the releasing lease.
- Add regression tests under
worker/test/fleet.test.ts and provider-specific
tests for crafted non-admin providerKey values, AWS existing-key handling,
and cleanup refusing to delete a key that is not bound to the lease.
Validation
Validation method: static source review of the verification target checkout at
6dba4afd1c5a4be0a780cf035d1b397e8982d478.
Evidence:
- Maintainer policy:
SECURITY.md defines destructive provider actions against
resources Crabbox cannot strongly identify as its own as in scope, while
excluding only trusted-operator/project-configuration behavior without a
separate Crabbox boundary bypass.
- Operations guidance:
docs/security.md states that the coordinator owns
authentication, authorization, lease state, provider credentials, cost
guardrails, and cleanup.
- Public request shape:
worker/src/types.ts:124-194 includes providerKey in
LeaseRequest.
- Non-admin reachability:
worker/src/fleet.ts:867-872 routes POST /v1/leases to createLease; worker/src/fleet.ts:1645-1671 has no
non-admin rejection for ordinary providerKey values.
- Source/control/sink:
worker/src/config.ts:316 copies the request value,
worker/src/fleet.ts:1757 stores it, worker/src/aws.ts:1018-1035 and
worker/src/aws.ts:1067-1072 use it as an EC2 key-pair identity, and
worker/src/fleet.ts:15607-15608 deletes matching AWS key-pair names during
release cleanup.
- Hetzner counterevidence and residual risk:
worker/src/hetzner.ts:103-108
rejects mismatched existing public keys, which blocks the most direct
Hetzner collision path. The same file still uses providerKey as the
provider SSH-key name (worker/src/hetzner.ts:97-151,
worker/src/hetzner.ts:262-269), and the coordinator cleanup path deletes
matching Crabbox-pattern key names by raw lease value
(worker/src/fleet.ts:14949-14953).
- Normal-client counterevidence: the stock Go coordinator backend sets
cfg.ProviderKey = providerKeyForLease(leaseID) before sending the lease
request (internal/cli/provider_coordinator.go:41-59), so exploitation
requires a crafted request or modified client rather than the default CLI
flow.
No dynamic proof was run in this child verifier. The verdict is based on exact
source/control/sink evidence and policy-boundary review.
[security] Non-admin lease creation can choose provider key names later treated as Crabbox-owned cleanup targets
Summary
The coordinator's
POST /v1/leasesroute acceptsproviderKeyfrom anyauthenticated caller and stores it as the lease's provider key without an
admin gate, per-owner namespace, or validation against the lease id. The
normal Go CLI derives this value from the lease id before sending the request
(
internal/cli/provider_coordinator.go:41-59,internal/cli/lease.go:155-160), and the documentation describes brokeredprovider keys as stable per-lease names (
docs/features/ssh-keys.md:51-62).The HTTP API itself does not enforce that invariant.
That makes
providerKeya non-admin-controlled provider-resource identifier.When it matches Crabbox's cleanup pattern (
crabbox-cbx-[a-f0-9]{12}), theAWS and Hetzner release paths later treat the name as a Crabbox-managed key
and delete it under the broker's provider credentials. This crosses the
maintainer-documented boundary for destructive provider actions against
resources Crabbox cannot strongly identify as its own: a low-privilege
coordinator user can cause provider key-pair or SSH-key deletion using a name
that was not derived from their lease.
The original draft overstated two details that are not supported by current
source. First, cleanup is not triggered for arbitrary names such as the legacy
default
crabbox-steipete; both AWS and Hetzner cleanup require thecrabbox-cbx-<12 hex>pattern. Second, deleting a provider key object does notby itself prove loss of SSH access to an already-running machine, because cloud
providers usually inject the public key at instance creation time. The
source-backed impact is unauthorized mutation/deletion of provider-side
Crabbox key records and identity confusion in broker-managed provider
metadata.
Affected Components
6dba4afd1c5a4be0a780cf035d1b397e8982d478cleanup.
worker/src/types.ts:124-194includesproviderKeyin the publicLeaseRequestbody.worker/src/fleet.ts:867-872exposesPOST /v1/leasesas a non-adminroute.
worker/src/config.ts:112-120andworker/src/config.ts:255-327build aLeaseConfigdirectly from the request body;providerKeyis assignedfrom
input.providerKey ?? "crabbox-steipete"atconfig.ts:316.worker/src/fleet.ts:1645-1671reserves only the workspace provider-keyprefix and admin-gates native image sources and host pinning. It does not
reject or derive caller-supplied ordinary
providerKeyvalues.worker/src/fleet.ts:1737-1758stores the chosenconfig.providerKeyinthe lease record.
worker/src/fleet.ts:12771-12773defines the cleanup-trusted key patternas
crabbox-cbx-[a-f0-9]{12}.worker/src/aws.ts:1018-1035accepts an existing EC2 key pair by namewithout verifying that its public key matches the lease's submitted public
key, and
worker/src/aws.ts:1067-1072uses that same name as the EC2KeyName.worker/src/fleet.ts:15607-15608deletes an AWS EC2 key pair on releasewhenever the stored lease
providerKeymatches the Crabbox cleanuppattern.
worker/src/hetzner.ts:97-151usesproviderKeyas the Hetzner SSH-keyname for lookup/create/delete. Hetzner does compare public-key identity
during creation (
hetzner.ts:103-108), which reduces the collision impact,but cleanup still deletes by raw matching name when the stored key passes
the coordinator cleanup predicate (
worker/src/fleet.ts:14949-14953).worker/src/provider-labels.ts:4-23stores the caller-selected providerkey in provider labels, so provider metadata no longer reliably reflects a
broker-derived per-lease identity.
Attack Path
Attacker role: authenticated non-admin coordinator user with a signed user
token or shared operator token.
Prerequisites:
POST /v1/leasesrequest rather than usingthe stock Go CLI, which normally derives
ProviderKeyfrom the new lease id.providerKeymatching theCrabbox cleanup pattern and colliding with a provider key pair that already
exists in the broker's AWS account.
Steps:
POST /v1/leasesas a non-admin caller with a body containing achosen
providerKey, for example acrabbox-cbx-<12 hex>value associatedwith another lease or a guessed Crabbox-managed provider key.
leaseConfigcopies the value intoconfig.providerKey(
worker/src/config.ts:316), andcreateLeasestores it in the lease record(
worker/src/fleet.ts:1737-1758). The ordinary non-admin guard atworker/src/fleet.ts:1645-1671does not reject the value unless it uses thereserved workspace prefix.
ensureSSHKeycallsDescribeKeyPairsand returns successfully ifthe chosen key name already exists (
worker/src/aws.ts:1018-1021), withoutcomparing the submitted public key to the existing EC2 key pair. The
instance launch then uses the attacker-selected
KeyName(
worker/src/aws.ts:1067-1072).AWS cleanup calls
DeleteKeyPairfor the storedlease.providerKeyif itmatches
crabbox-cbx-[a-f0-9]{12}(worker/src/fleet.ts:15607-15608,worker/src/aws.ts:1003-1009).(
worker/src/hetzner.ts:103-108), so the same collision is harder to turninto a successful active lease. However, the API still accepts the
non-admin-selected provider key as the provider SSH-key name, and successful
leases with cleanup-pattern names are later deleted by raw name
(
worker/src/fleet.ts:14949-14953,worker/src/hetzner.ts:142-151).Control/data flow:
Impact
A non-admin coordinator caller can make the broker perform provider key
operations against an identifier that is not derived from the caller's lease.
For AWS, the source proves that an existing EC2 key pair name is accepted
without public-key verification, used for instance launch, and then deleted on
release if it matches Crabbox's cleanup pattern. For Hetzner, public-key
identity comparison prevents the direct mismatched-key creation path, but the
same unvalidated identifier still reaches the provider key namespace and
cleanup code.
The practical impact is provider-resource integrity and availability damage:
Crabbox can delete or mislabel provider-side SSH key resources that it cannot
strongly bind to the releasing lease. This matches the in-scope policy example
for destructive provider actions against resources Crabbox cannot strongly
identify as its own. The issue does not expose private SSH keys, and current
source does not prove that deleting the provider key object removes SSH access
from already-running instances, so the severity should stay below the original
draft's high-impact availability claim.
Severity Assessment
CVSS Assessment
The score reflects a network-reachable authenticated non-admin caller, no user
interaction, no confidentiality impact, and limited integrity/availability
impact to provider-side key-pair identity and lifecycle consistency. The
score is not higher because current source does not prove private-key exposure,
admin-token compromise, cross-owner coordinator data access, or guaranteed SSH
loss on already-running machines.
Recommended Remediation
providerKeyfrom ordinaryPOST /v1/leasesrequests. Fornon-admin requests, derive the provider key server-side from the final lease
id, matching the documented
providerKeyForLeaseformat(
crabbox-<lease id>with underscores rewritten to dashes).still need to supply a provider key manually. That path should reject
crabbox-cbx-*names that do not correspond to the lease being created, orotherwise record ownership metadata that cleanup can verify before deletion.
public key before treating it as reusable. If AWS cannot return enough key
material to prove a match, fail closed or require an admin-only override.
name is derived from the same lease id or when stored ownership metadata shows
Crabbox created that exact key for the releasing lease.
worker/test/fleet.test.tsand provider-specifictests for crafted non-admin
providerKeyvalues, AWS existing-key handling,and cleanup refusing to delete a key that is not bound to the lease.
Validation
Validation method: static source review of the verification target checkout at
6dba4afd1c5a4be0a780cf035d1b397e8982d478.Evidence:
SECURITY.mddefines destructive provider actions againstresources Crabbox cannot strongly identify as its own as in scope, while
excluding only trusted-operator/project-configuration behavior without a
separate Crabbox boundary bypass.
docs/security.mdstates that the coordinator ownsauthentication, authorization, lease state, provider credentials, cost
guardrails, and cleanup.
worker/src/types.ts:124-194includesproviderKeyinLeaseRequest.worker/src/fleet.ts:867-872routesPOST /v1/leasestocreateLease;worker/src/fleet.ts:1645-1671has nonon-admin rejection for ordinary
providerKeyvalues.worker/src/config.ts:316copies the request value,worker/src/fleet.ts:1757stores it,worker/src/aws.ts:1018-1035andworker/src/aws.ts:1067-1072use it as an EC2 key-pair identity, andworker/src/fleet.ts:15607-15608deletes matching AWS key-pair names duringrelease cleanup.
worker/src/hetzner.ts:103-108rejects mismatched existing public keys, which blocks the most direct
Hetzner collision path. The same file still uses
providerKeyas theprovider SSH-key name (
worker/src/hetzner.ts:97-151,worker/src/hetzner.ts:262-269), and the coordinator cleanup path deletesmatching Crabbox-pattern key names by raw lease value
(
worker/src/fleet.ts:14949-14953).cfg.ProviderKey = providerKeyForLease(leaseID)before sending the leaserequest (
internal/cli/provider_coordinator.go:41-59), so exploitationrequires a crafted request or modified client rather than the default CLI
flow.
No dynamic proof was run in this child verifier. The verdict is based on exact
source/control/sink evidence and policy-boundary review.