Skip to content

[security] Non-admin lease creation can choose provider key names later treated as Crabbox-owned cleanup targets #706

Description

@coygeek

[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:

  1. 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.
  2. 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.
  3. 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).
  4. 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).
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Urgent regression or broken agent/channel workflow affecting real users now.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions