AWS Image Qualification Authority
September 5, 2026 ยท View on GitHub
This is a pre-merge test boundary for a dedicated AWS sandbox account. It lets an exact candidate coordinator execute Crabbox Fleet authentication, serialization, AWS provider lifecycle, image catalog compare-and-swap, revision retirement, and publisher rollback without receiving AWS or Cloudflare credentials.
It is not a general AWS proxy and is not enabled by default.
Trust boundary
There are four Workers:
- A protected controller selects the named
AWSQualificationControllerentrypoint to enroll, fence, and finalize runs. - The exact candidate Worker has no public endpoint. It has only
CRABBOX_AWS_QUALIFICATION_TRANSPORT, bound to the authority's default transport entrypoint. - A trusted narrow relay is the only workers.dev endpoint exposed to the credentialless executor. Its sole service binding targets the private candidate Worker.
- The non-public authority Worker holds the sandbox AWS credentials and signs admitted calls.
The relay accepts a distinct ephemeral executor token and injects the candidate
admin token only for the fixed lease, image, and run API methods needed by the
publisher proof. One separate path injects the candidate shared token only for
the /promote-cas spoofed-admin probe. It rejects unknown paths, methods,
queries, content types, oversized bodies, and identity/header overrides before
calling the candidate. Callers cannot choose a target Worker, forwarding
headers, or destination URL. The relay has no controller or authority binding
and receives no AWS or Cloudflare credential. It strips literal candidate auth
tokens from bounded responses before returning them to the executor.
The relay rejects an absent or expired run timestamp and rechecks expiry
immediately before candidate dispatch.
The publisher proof records a seeded base revision and failed candidate revision. It requires the rollback receipt to restore the exact seeded default aliases and revision, retire the failed candidate revision, and reject a stale compare-and-swap request naming that failed revision. Full candidate API readbacks before and after that stale request must match, including catalog, default, and Fast Snapshot Restore state. A retired AMI may remain visible as a matching provider-only record, but it must have no revision, promotion timestamp, or catalog-only marker.
Protected teardown first persists the authority and registry finalization fence, then disables and verifies the relay's public endpoint and deletes the relay Worker before running idempotent authority cleanup. Requests already admitted by the relay cannot reach the AWS signer after that fence, and later requests have no public credential-injection path to the private candidate.
The candidate binding has immutable ctx.props containing runId, owner,
candidateSha, candidateWorker, deploymentHash, and expiresAt. The
controller binding carries the same deployment hash. Enrollment stores the
complete fixed policy and its hash plus the authority source SHA and version;
candidate operations fail closed if the deployed policy later drifts.
The deployment hash also commits to the trusted relay source, fixed bindings,
candidate service target, and digests of all three ephemeral tokens. The final
execution manifest additionally binds the exact deployed candidate and relay
Worker versions.
The authority requires an enrolled exact match.
The absolute expiry must be in the next 120 minutes. A transport failure retries
the same operation ID once and then fails closed; it never falls back to raw
credentials, even if stray credential variables are present.
The authority accepts a structured service, action, and parameter map. It never accepts a caller-selected URL, HTTP method, headers, or encoded body. It rebuilds the AWS request after policy validation.
Fixed sandbox policy
Configure these authority variables:
CRABBOX_AWS_QUALIFICATION_ACCOUNT_ID
CRABBOX_AWS_QUALIFICATION_AUTHORITY_SHA
CRABBOX_AWS_QUALIFICATION_AUTHORITY_VERSION
CRABBOX_AWS_QUALIFICATION_REGION
CRABBOX_AWS_QUALIFICATION_SUBNET_ID
CRABBOX_AWS_QUALIFICATION_SECURITY_GROUP_ID
CRABBOX_AWS_QUALIFICATION_BASE_AMI_ID
CRABBOX_AWS_QUALIFICATION_ROOT_GB
The account, Region, subnet, preprovisioned security group, and base AMI are
fixed by the authority deployment. CRABBOX_AWS_QUALIFICATION_ROOT_GB must be
8-20. A run may launch at most three sequential on-demand t3.small or
t3a.small instances, with only one active at a time. This permits the source,
candidate-image, and promoted-image smoke phases while bounding compute to
at most 120 minutes of aggregate instance runtime.
Each launch has an encrypted
gp3 root volume, no instance profile, IMDSv2 required, and authority-injected
owner/run/SHA/expiry/operation tags.
The run may own one physical key pair and one active AMI with at most one child snapshot. Candidate traffic is capped at 64 operations, eight unresolved intents, and the 120-minute expiry. The authority accepts only the five request fields in the service binding contract, bounds the complete envelope to 64 KiB before normalization, and requires a flat map of at most 256 string parameters. Responses are also capped at 64 KiB. Cleanup and inventory calls do not consume candidate capacity.
The preprovisioned security group is read-only to qualification runs. Its ingress must already admit the trusted smoke executor. The authority neither creates nor edits security groups, so final verification proves that the configured group still exists rather than claiming that it was ephemeral.
Fast Snapshot Restore is rejected by the Worker. The AWS permissions boundary or SCP must also explicitly deny:
ec2:EnableFastSnapshotRestores
ec2:DisableFastSnapshotRestores
Do not grant iam:PassRole. The authority role's allow surface is limited to:
sts:GetCallerIdentity
servicequotas:GetServiceQuota
ec2:CreateImage
ec2:CreateTags
ec2:DeleteKeyPair
ec2:DeleteSnapshot
ec2:DeregisterImage
ec2:DescribeImages
ec2:DescribeInstances
ec2:DescribeKeyPairs
ec2:DescribeSecurityGroups
ec2:DescribeSnapshots
ec2:DescribeVolumes
ec2:ImportKeyPair
ec2:RunInstances
ec2:TerminateInstances
Apply aws:RequestedRegion to all regional allows. Create and tag allows must
require the crabbox_qualification_run, crabbox_qualification_owner,
crabbox_qualification_sha, and crabbox_qualification_expiry request tags.
Mutation of existing resources must require the same run resource tag. Restrict
RunInstances to the configured base AMI ARN or an AMI carrying the complete
authority-injected qualification tag set, plus the configured subnet and
security group ARNs and the two allowed instance types. Do not grant a wildcard
AMI resource. IAM provides the outer tagged-image boundary; the authority then
requires the exact derived AMI ID to remain active in the enrolled run ledger,
so another run's or an unrelated tagged image is still rejected. Keep these IAM
constraints even though the authority duplicates them at runtime.
Enrollment and binding
Deploy the authority from
worker/wrangler.aws-qualification-authority.jsonc. That config has no route,
workers.dev URL, preview URL, or cron. Supply AWS credentials only to this Worker.
Before deploying a candidate, bind the protected caller to
AWSQualificationController with the reviewed deployment hash and call
enroll. Add a candidate service binding whose props exactly match the enrolled
identity:
{
"binding": "CRABBOX_AWS_QUALIFICATION_TRANSPORT",
"service": "crabbox-aws-qualification-authority",
"props": {
"runId": "image-qualification-<run>",
"owner": "<reviewed-owner>",
"candidateSha": "<40-hex-same-repo-sha>",
"candidateWorker": "<isolated-candidate-worker-name>",
"deploymentHash": "<64-hex-candidate-deployment-hash>",
"expiresAt": "<absolute-iso8601-within-120m>"
}
}
The candidate configuration must use the same fixed Region, AMI, subnet,
security group, and root size; request on-demand; select only t3.small or
t3a.small; leave CRABBOX_AWS_INSTANCE_PROFILE empty; and leave
CRABBOX_AWS_FAST_SNAPSHOT_RESTORE_AZS unset. It must also disable routes,
workers.dev, preview URLs, and cron. Protected tooling first uploads an inert
bootstrap, disables workers.dev and verifies the setting, and only then uploads
candidate bytes. The inert bootstrap already carries the run-scoped transport
identity and Durable Object migration, so a finalizer or reaper can discover
and delete it after a partial deployment. The relay enables workers.dev with
preview URLs disabled and also has no routes, custom domain, cron, or Durable
Object namespace.
The protected controller also owns a singleton AWSQualificationRegistry
Durable Object. claim admits only one active qualification globally and is
idempotent only for the exact run, candidate Worker, candidate SHA, deployment
hash, and expiry. discover gives an independent reaper the active run and its
claimed, finalizing, or finalized cleanup state without relying on workflow
artifacts. beginFinalization persists the fence before external ingress
teardown, and finalize updates that state around per-run cleanup. retire clears
the active slot only after the persisted run attestation proves finalization,
is idempotent for the same bounded retirement tombstone, and rejects a different
active run. A retired or finalizing per-run object cannot be enrolled again.
These methods and attest exist only on the named controller entrypoint; the
candidate transport exposes only execute. The controller persists the per-run
cleanup owner before publishing its global claim, and every candidate call must
match that exact registry record while its state remains claimed. Finalization
persists an irreversible per-run finalizingAt fence before transitioning the
registry to finalizing or starting cleanup I/O, so an already-admitted
candidate call and a failed cleanup cannot reopen candidate dispatch.
The relay name is deterministically derived from the registered run ID, so the
durable registry identity is sufficient for a fresh finalizer or reaper to
discover and delete the exact relay without workflow artifacts.
attest(runId) returns versioned evidence built from persisted Durable Object
state. It binds the run, candidate and authority revisions, deployed bundle and
policy hashes, timestamps, and finalized state. Each bounded operation record
contains only operation/request digests, action, a normalized denial reason,
and persisted signer before/after sequence points. The final receipt records
resource counts and pending intent digests at cleanup start, every bounded
cleanup attempt, each inventory and verification outcome, final zero counts,
and normalized failures. Cleanup, inventory, and verification evidence use
bounded rings with total and truncation counters; saturation drops the oldest
proof records without blocking teardown. The attestation never returns raw
tokens, keys, user data, URLs, account IDs, network addresses, or AWS resource
IDs.
Replay and teardown
Each run has one Durable Object. Before a mutation, it persists the operation ID,
canonical normalized-request hash, and a prepared intent, then marks the intent
dispatched immediately before the signer call. Prepared intents are deleted
without recovery. Finalization uses action-specific read reconciliation for
dispatched or legacy ambiguous launch, termination, image, and key intents. It
never redispatches generic candidate mutations; those intents remain until
inventory, cleanup, and zero-residue verification allow their retirement.
A completed receipt is replayed only for the same hash. RunInstances receives
an authority-derived deterministic ClientToken derived from the run and
operation IDs. Uncertain CreateImage and ImportKeyPair results retain their
intents and use bounded read reconciliation; they are never blindly reissued.
Image reconciliation uses authority-injected operation tags. Imported key names
are authority-generated; ownership is recorded only after ID, public key, and
run tags are read back.
If bounded reconciliation proves neither success nor a definitive error, the
intent remains until final inventory and cleanup prove that no run-owned
resource remains; only then is it retired.
Finalization never redispatches a pending RunInstances request. It performs
bounded read-only discovery by the authority-injected run and operation tags,
then cleans any discovered instance; a no-effect launch intent is retired only
after the same zero-residue proof. Lost termination responses use bounded
single-instance reads and require repeated absence or an explicit terminal state
before the intent and active slot are retired.
The ledger learns only IDs created by, or discovered beneath, the registered
run. Candidate reads and mutations are restricted to those IDs, except the fixed
base AMI and security group. Lifecycle deletes remove current ledger ownership
instead of accumulating stale IDs; terminating instances remain active until a
single-ID read confirms terminated or absent. Before and after teardown, bounded
eventual-consistency inventory scans find resources that appeared after an
ambiguous response. The authority revalidates the configured STS account before
every mutation, so credential rotation cannot move a run into another account.
The public AWS release path waits for that terminal instance state only when the
qualification transport binding is active; ordinary AWS releases keep their
existing fire-and-forget behavior.
Successful deletes move exact instance, key-pair, AMI, and snapshot IDs into
tombstone sets bounded by the launch and candidate-operation limits. Tombstones
do not consume active-resource capacity or permit new use, but they preserve
exact verification reads and idempotent cleanup retries while continuing to
reject foreign IDs.
finalize
deregisters images, deletes snapshots,
terminates instances, deletes imported key pairs, and verifies zero run-owned
instance/volume/key-pair/image/snapshot residue. The expiry alarm runs the same
cleanup and retries incomplete teardown. Protected teardown fences candidate
mutations, deletes the public relay, deletes the private candidate Worker and
its Fleet Durable Object namespace, verifies their absence, repeats authority
finalization idempotently, and retires the registry record.
Cloud setup, enrollment, candidate deployment, and paid execution remain explicit maintainer actions. Merging this seam creates no Worker, secret, Durable Object namespace, AWS resource, or spend.