security(operator): fail closed without OPERATOR_API_TOKEN #190
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#190
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
packages/operator/src/api.rstreatsOPERATOR_API_TOKENas optional.start_api_serverloads it withstd::env::var(...).ok().filter(|t| !t.is_empty())and storesOption<Arc<str>>.check_authreturnstruewhen the option isNoneor empty.GET /statusandGET /pendingtherefore serve queue data with noAuthorizationheader whenever the env var is missing.That is fail-open. Production-like config (
.env.exampledoes not require the var; unit testtest_check_auth_no_token_configuredasserts the open path) still binds those routes.Module docs state the same:
/statusand/pendingare “auth-gated whenOPERATOR_API_TOKENis set”. Closed O2 inpackages/operator/security_reviews/SECURITY_REVIEW_2026-02-12.mdmarked optional bearer gating as “Fixed”. Residual: unset still means unauthenticated.Default listen is
127.0.0.1:9092.OPERATOR_API_BIND_ADDRESScan be0.0.0.0. The API has no TLS (packages/operator/README.md). Rate limits (RATE_LIMIT_PER_SECOND/ burst) do not replace auth.This is not #115 (RPC quorum /
/healthstaleness). This is not #178 (EVMguardBridge/rateLimitBridgezero-address). This is not #184 (EVM deposit nonce lookup). This is not #187 (CI skip fail-closed). Keyword overlap on “fail closed” / “operator” / “pending” is not this bug.Internal review id: RS-M3 (medium). Still in source as of 2026-09-12 (
packages/operator/src/api.rsonmain).Bundle (same ticket, do not split):
OPERATOR_API_TOKENis missing or empty, unless an explicit local/dev flag is set.check_authand intest_check_auth_no_token_configured./statusor/pendingas unauthenticated JSON. Local/E2E must set a test token (preferred) or the documented dev flag — not leave the routes open..env.example/ security-review residual: token is required in production; optional O2 is not a complete fix.Founder-required operator auth / keys. No community autoland. Do not add
ready.Impact (today vs hypothetical)
Auth and operational confidentiality at risk today in source, and on any live operator whose process started without
OPERATOR_API_TOKEN./pendingreturns up to 50 pending/submitted approvals and releases withid,nonce,recipient,amount,status./statusreturns pending deposit/approval/release counts. That is enough to map in-flight bridges (who, how much, which nonce) without holding an operator key.This is not a user-facing puzzle that needs an exploit recipe. Anyone who can reach the listen address can
GETthose paths. Default bind is loopback, so remote reach needs a non-local bind, a proxy, or a local process. Source still permitsOPERATOR_API_BIND_ADDRESS=0.0.0.0and still serves the privileged routes with no token. Docs already warn about plaintext when the token is set; they do not refuse to start when it is not.Hypothetical-only if every production process already has a non-empty token and cannot bind without it. Source and unit tests still treat absence as open access. Sticky until startup refuses the empty path and tests prove production-like config never registers unauthenticated
/status//pending.Do not publish a live-operator probe, bind inventory, or copy-paste request against a public listen address.
check_authfail-openstatus_handler/pending_handlercall this before any DB read. MissingAuthorizationis401only when a token was loaded.Startup never requires the var
start_api_serverlogs only when the token is set. Empty and unset both becomeNone. Router always.route("/status", ...).route("/pending", ...). Bind happens either way..env.exampledocuments DB, RPC, keys, and rate limits. It does not listOPERATOR_API_TOKEN. A production copy of the example starts with open privileged routes.Unit tests lock in the vuln
test_check_auth_no_token_configuredassertscheck_auth(&empty, None)andcheck_auth(&empty, Some(""))aretrue. There is no test that production-like config refuses to listen, or that/status//pendingare unregistered without a token.Invariants
GET /statusorGET /pendingunlessOPERATOR_API_TOKENis non-empty. Missing/whitespace-only token → fail startup of the API server (do not listen).GET /statusandGET /pendingare401and return no queue JSON (no nonce, recipient, or amount).GET /healthandGET /metricsmay stay public liveness/scrape surfaces. They must not grow queue rows or recipient/amount fields.WithdrawApprove, watcher finality, or cancel windows. Do not put live tokens in fixtures, docs, or issue comments. Do not require TLS inside this ticket (README already requires a terminating proxy if bound beyond localhost).Constraints / guardrails
start_api_server/mainbeforeTcpListener::bind. Do not keep a silent “open if unset” path.Some("")as configured./health(liveness / idle 503) and/metricspublic unless a later ticket scopes scrape auth. Do not dumpPendingResponseonto/health.DEV_ALLOW_HTTP). Document it as local/E2E only. Do not use “bind is 127.0.0.1” as the substitute for a token in production-like tests.packages/frontend/src/test/e2e-infra/operator.ts) and QA env helpers must set a test token or the dev flag. Prefer a test token so CI exercises the401path.401bodies.ready. No public live-API recipe.Relevant files
packages/operator/src/api.rscheck_authreturns true when unset;/statusand/pendingalways routed; unit tests encode fail-openpackages/operator/src/main.rsOPERATOR_API_BIND_ADDRESS/ port; callsstart_api_serverpackages/operator/.env.examplepackages/operator/README.mdOPERATOR_API_TOKENis set”; TLS notepackages/operator/security_reviews/SECURITY_REVIEW_2026-02-12.mddocs/operator.md(if present)packages/frontend/src/test/e2e-infra/operator.tsscripts/qa/qa-host.envRecommended direction
OPERATOR_API_TOKENbeforeTcpListener::bind. Return an error fromstart_api_server(and fail process start) when it is missing, unless the documented local-only flag is set.check_auth: configured token required for/statusand/pending. Missing/wrong Bearer →401, empty body. Invert or deletetest_check_auth_no_token_configured.start_api_servererrors and does not accept connections on/statusor/pending. Optionally: router built for that config does not register those routes.401with no queue fields./status//pending, or keep them local-debug only and not used by production-like tests. E2E should set a test token..env.example, and the O2 residual note: optional gating is not production-complete.Acceptance criteria
OPERATOR_API_TOKEN(and without the local-only flag) does not bindGET /statusorGET /pending. API server start fails closed.OPERATOR_API_TOKENare treated as missing (AC1).401and nononce/recipient/amountin the body for/statusand/pending./statusqueue counts and/pendinglists still work./healthand/metricsremain reachable without the token.test_check_auth_no_token_configuredno longer asserts open access as success. New tests cover AC1–AC4 without a live operator host..env.example/ security-review residual state that production requires the token. O2 optional gating is not the end state.Test plan (functional paths)
GET /status+ valid BearerGET /pending+ valid BearerGET /statuswith noAuthorizationGET /pendingwith wrong BearerGET /healthwith no tokenGET /metricswith no tokenstart_api_server/ process errors; no listener serving T3/T4 as 200OPERATOR_API_TOKEN=""or whitespaceTest plan (attack, hack, and abuse)
Non-exploitative. Operator unit / in-process router tests only. Do not use these as a live listen-address recipe.
/statusor/pending(no bind or fail start)AuthorizationmissingAuthorization: Bearer+ wrong secretAuthorization: Basic …or noBearerprefixtest_check_auth_no_bearer_prefix)bearervsBearerprefix0.0.0.0in a test config with token missing and no local flagVerification criteria
check_authno longer returnstrueforNone/ empty in production-like builds.start_api_server(ormain) errors when the token is missing unless the documented local flag is set..env.example/ README for requiredOPERATOR_API_TOKEN(or an explicit “required unless<dev flag>” sentence).test_check_auth_no_token_configuredas a success case for open access.Out of scope
/healthidle 503 / RPC quorum /FINALITY_BLOCKS./metricsscrape (optional later; not this ticket).First-pass model recommendation
Recommendation: grok-high
Rationale: Security class and founder-required operator API auth / keys. Composer is disallowed (High/security; not a low-risk first pass). Even if the production edit is likely
api.rsplus startup inmain.rsplus tests/docs, file count does not establish safety: fail-open auth on/pendingleaks in-flight nonces, recipients, and amounts, and the existing unit test encodes the wrong default. Verify with in-process start/router fixtures (token missing vs set), not a live operator probe.