Skip to content

fix(admin): require an admin token for /_cloudemu under --enforce-auth - #1438

Merged
NitinKumar004 merged 3 commits into
developmentfrom
fix/admin-endpoints-enforce-auth
Oct 4, 2026
Merged

NitinKumar004 merged 3 commits into
developmentfrom
fix/admin-endpoints-enforce-auth

Conversation

@NitinKumar004

Copy link
Copy Markdown
Collaborator

Summary

Fixes AUTHN-X3. Under serve --enforce-auth the /_cloudemu/* control plane needed no credentials, so curl :4566/_cloudemu/snapshot returned every IAM secret access key, and an anonymous POST to snapshot, seed or reset could replace all state, including injecting IAM keys. The SigV4 gate added by --enforce-auth only covered the cloud APIs.

What changed

  • Admin token gate (server/admin). Control.RequireToken makes every control endpoint except health require Authorization: Bearer <token> and return 401 with a WWW-Authenticate: Bearer challenge otherwise. Both sides are hashed with SHA-256 and compared with crypto/subtle, so the check is constant time and doesn't leak the length. Requests outside /_cloudemu/ go to the backend untouched.
  • Wiring (server/serverkit). With EnforceAuth and Admin both on, New takes Config.AdminToken or generates a random 32-byte token. If Config.AdminTokenFile is set, the token goes to that file (mode 0600) and only the path is logged. Otherwise a generated token is printed once on stderr. Every listener's Control (AWS, Azure, GCP, OCI, Kubernetes) gets the same token. The non-loopback --admin warning no longer fires under --enforce-auth, since the plane is now gated.
  • Flags (server/serveflags). --admin-token (env CLOUDEMU_ADMIN_TOKEN) and --admin-token-file (env CLOUDEMU_ADMIN_TOKEN_FILE). They're shared, so cloudemu serve and contrib/server both get them. -h never prints the token, even when it comes from the env. The help for --admin-token documents the bootstrap.
  • First IAM user bootstrap (seed, AWS IAM). Under --enforce-auth you can't call CreateAccessKey without a key to sign with, so there was no in-band way to get the first key (earlier e2e runs worked around this by restoring a snapshot). Seed fixtures now take iamUsers with access keys whose id and secret you choose. An optional iamdriver.AccessKeyImporter capability (AWS only, like AccessKeyResolver) registers them, with the same per-user quota and duplicate checks as CreateAccessKey. A user with no policies is unrestricted, so that user can create everyone else through the normal IAM API. This works through POST /_cloudemu/seed with the admin token and through --init-dir.
  • In-repo clients. cloudemu start passes --admin-token-file ~/.cloudemu/admin-token, removes any stale token before launching, and delete removes the file. cloudemu snapshot save|load, net and cost send the token from CLOUDEMU_ADMIN_TOKEN or from that file, and a 401 turns into a clear error. In testcontainers, WithEnforceAuth(token) starts the container with --enforce-auth and waits on /_cloudemu/health (unsigned GET / returns 403 under enforce-auth). Reset and Seed send the token. The contrib/server enforce-auth tests now bootstrap through the seed path instead of a second auth-off server.
  • Docs. contrib/server/README.md has a new "Admin token and the first IAM user" section. docs/standalone-server.md and docs/persistence.md and the testcontainers README are updated too.

Design choice

I went with a bearer admin token (option a) over SigV4 from an admin principal:

  • It's the same for every cloud's port. The Azure, GCP, OCI and Kubernetes listeners all expose /_cloudemu, and SigV4 only means something on the AWS side.
  • It avoids the chicken-and-egg problem. With SigV4 you'd need an IAM key to create the first IAM key, and the token is what breaks that loop.
  • The tooling can find it without extra work: start writes it to the run dir, the CLI reads it, and testcontainers passes it through the env.

Behaviour with --enforce-auth off is unchanged: the control plane stays open and the token flags are ignored. That's still the documented developer mode. With auth on and a valid token, the snapshot GET still returns secrets, because restore needs them.

health stays open on purpose. There's no Dockerfile HEALTHCHECK. cloudemu start uses a TCP probe, and testcontainers waits on GET / by default or on /_cloudemu/health with WithEnforceAuth.

Testing

  • New tests were written first and fail on the old code: TestEnforceAuthAdminEndpointsRequireToken in serverkit got 200/400 instead of 401 on every gated endpoint.
  • server/admin: missing, wrong, malformed and non-Bearer tokens get 401 and no body leaks. A valid token gets 200. health is open, cloud API paths pass through, and with no token set the plane stays open.
  • server/serverkit: enforce-auth gating on every listener, a fixed token, a generated token written 0600, auth-off unchanged, a seeded IAM key resolvable by SigV4, and the warning.
  • server/serveflags: the token comes from the env, the flag wins over the env, and -h never shows the value.
  • cmd/cloudemu: snapshot save/load and cost against an in-process --enforce-auth server, using the run-dir token and CLOUDEMU_ADMIN_TOKEN. Without a token, or with a wrong one, they fail with the 401 error.
  • contrib/server: TestEnforceAuthAdminEndpointsNeedToken runs the full flow with the real SDK. Unauthenticated calls get 401 and health is open. It seeds the first user, signs CreateUser with that user's key, then snapshot, reset and restore all work with the token.
  • seed and providers/aws/iam: iamUsers apply and validation, and ImportAccessKey checks for user existence, duplicates and quota.
  • Gates: go build ./..., then go vet and go test -race on admin, serverkit, serveflags, cmd/cloudemu, persist, seed and aws/iam. contrib/server passes -run 'Enforce|Admin|Snapshot', and golangci-lint --new-from-rev is clean.

E2E against a real serve --enforce-auth --aws-port 45766:

  • Without a token, snapshot GET/POST, reset, seed, cost, net/can-connect and snapshot/ return 401, and health returns 200.
  • A wrong token also gets 401.
  • Seeding iamUsers with the token, then calling aws iam create-user, sts get-caller-identity and s3 mb with that key, all succeed. A bogus key gets InvalidClientTokenId.
  • With the token, the snapshot holds the secret, and reset then restore brings back both users.
  • cloudemu snapshot save/list/load and cost work with CLOUDEMU_ADMIN_TOKEN and fail cleanly without it.
  • cloudemu start --enforce-auth writes admin-token (0600), and snapshot save works with no env set. An unauthenticated reset returns 401.

The testcontainers module vets and builds, but its Docker test (TestEnforceAuthAdminToken) builds the image and the local Docker daemon wasn't responding, so it didn't run here. CI covers it.

Every control endpoint except health now needs Authorization: Bearer <token>
when serve runs with --enforce-auth. The token comes from --admin-token or
CLOUDEMU_ADMIN_TOKEN, or is generated and printed once or written to
--admin-token-file. Seed fixtures accept iamUsers with fixed access keys so
the first IAM user can be bootstrapped with the token. cloudemu start, the
snapshot/net/cost commands and testcontainers send the token.
Write the admin token through an O_EXCL temp file renamed into place,
insert imported access keys with SetIfAbsent, require AKIA-shaped key ids
and bounded secrets in seed fixtures, and let testcontainers generate the
token and append --enforce-auth to an existing command.
…oints-enforce-auth

# Conflicts:
#	providers/aws/iam/iam.go
@NitinKumar004
NitinKumar004 marked this pull request as ready for review October 4, 2026 10:50
@NitinKumar004
NitinKumar004 merged commit a4dcd0a into development Oct 4, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant