When to Use
Use this skill when a trading application runs in dual modes — paper/sandbox and live production — and a single misrouted request would either burn real capital or silently reject a paper order. CredentialEnvironmentGuard.validate_request_boundary() is a veto gate called immediately before each outbound broker HTTP call: it parses the destination URL and requires the hostname (plus a path prefix, where the venue separates environments by path) to positively match an endpoint declared for the environment the process says it is running in.
The design decision that matters is allow-list, not deny-list. A guard built as "reject the URL if it contains a known live hostname" approves every destination its author did not think to enumerate — including look-alike domains, hosts the venue added after the rules were written, and any URL where the word paper happens to appear in a query parameter. This engine inverts that: the request is refused unless it is recognised. An unknown broker is a violation, not a pass.
Its second job is not becoming a leak itself. Broker URLs routinely carry secrets in the query string (Binance appends &signature=<hmac> to signed REST calls). Every URL reaching an exception message or a log line is stripped of userinfo, query, and fragment first.
When NOT to Use
- Single-endpoint venues: If one base URL serves both environments and only the credential differs, the endpoint allow-list has nothing to decide. Use credential-scope controls instead (
api-key-least-privilege-audit-tool). - As the only Alpaca control: For Alpaca specifically,
alpaca-paper-live-key-separationadds a liveGET /v2/accountprobe of theis_paperflag and anALLOW_LIVE_TRADINGgate. This guard validates the request boundary; it never contacts the broker and so cannot detect that correct-looking credentials belong to the wrong account. - Backtests and replay harnesses: No network calls to a broker means no boundary to enforce.
- Secret storage and rotation: This is a runtime egress check, not a vault. See
centralized-secrets-management-vault-integrationandsecrets-rotation-without-bot-downtime. - As a substitute for separate credential stores: The guard catches a mixed-up credential at the last moment. It does not excuse loading paper and live secrets into the same process.
Prerequisites
- An explicitly declared
TradingEnvironment(SANDBOXorPRODUCTION) sourced from deployment configuration, not inferred from a URL or a key. BrokerEnvironmentRulesper broker in use:broker_name,sandbox_endpoints,production_endpoints, and optionallysandbox_key_prefixes/production_key_prefixes. Endpoints are"host"or"host/path/prefix"shorthand, orEndpointRuleobjects.- Every outbound broker call routed through the guard. A single HTTP client that bypasses it defeats the control entirely.
Workflow
-
Declare the environment once, at process start.
- Construct
CredentialEnvironmentGuard(TradingEnvironment.SANDBOX | PRODUCTION). Passing anything other than the enum raisesTypeErrorrather than being coerced. - Leave
allow_unknown_brokersat its defaultFalse. Setting itTruedisables the boundary check for unregistered brokers and logs a warning each time — treat it as an explicit, reviewed risk acceptance, never a convenience default.
- Construct
-
Validate the destination URL structurally, before comparing it to anything.
- Reject non-
httpsschemes: anhttp://broker call sends the API key in cleartext headers. - Reject userinfo in the URL.
https://api.alpaca.markets@evil.example/v2/ordersreads as the Alpaca host and resolves toevil.example; credentials in a URL are the leak this skill exists to stop. - Reject a missing hostname and any port other than 443.
- Reject non-
-
Match the destination against the environment's allow-list, on parsed components.
- Compare the exact lower-cased hostname. Never a substring:
"api.alpaca.markets" in urlalso matcheshttps://api.alpaca.markets.attacker.example/v2/orders. - Where a venue separates environments by path rather than host, require the path prefix too. Saxo Bank is the shipped case — simulation is
gateway.saxobank.com/sim/openapiand live isgateway.saxobank.com/openapion the same hostname, so a host-only check treats live and sim as interchangeable. - Normalise the path before comparing, so
/sim/openapi/../../openapi/port/v1/ordersis judged as the live path it actually resolves to. - No match against this environment's list is a violation. If the destination matches the opposing environment's list, say so explicitly — that is the cross-environment leak, and it is the more actionable message.
- Compare the exact lower-cased hostname. Never a substring:
-
Treat the key prefix as corroboration, never as the decision.
- A key carrying the opposing environment's prefix (
AK…in sandbox mode) is a hard violation — that is a high-confidence positive signal. - A key matching none of the expected prefixes only logs a warning. Alpaca's authentication documentation specifies no key format at all, so the observed
PK/AKconvention is not a contract; Binance keys and Saxo OAuth tokens have no prefix whatsoever. Absence of a prefix proves nothing, and blocking on it would break every venue without a prefix scheme. - Compare prefixes case-insensitively: a lower-cased
ak_live_…is still a production-shaped key.
- A key carrying the opposing environment's prefix (
-
Redact before reporting. Strip userinfo, query, and fragment from every URL that reaches a
SecurityViolationErrormessage or a log record. Never log the API key; report only which prefix matched. -
Review the shipped allow-list on a schedule.
iter_declared_endpoints()dumps every(broker, environment, endpoint)triple for diffing against current vendor documentation. Venues add and retire hosts without changing their API version — Binance alone serves production spot from six hostnames.
Full procedure: see
references/workflows.md. Endpoint facts and citations: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Substring matching the URL instead of the parsed host.
"paper" in urlapproveshttps://api.alpaca.markets/v2/orders?client_tag=paper— a live order, in sandbox mode, blessed by the guard because an unrelated query parameter contained the word. Any allow or deny decision driven by attacker- or operator-controlled URL text is not a control. - Checking only "is not the other environment". Verifying that a production URL is not a sandbox URL leaves
https://attacker.example/v2/ordersapproved, and with it the live credential in the request headers. Both directions need a positive match. - Failing open on an unregistered broker. The case where no rules exist is precisely the case with no protection; returning
Truethere converts the guard into decoration. Fail closed and make the operator register the broker or opt out in writing. - Assuming a registrable domain identifies an environment. Binance serves production market data from
data-api.binance.visionand the spot testnet fromtestnet.binance.vision— same domain, opposite environments. - Treating an undocumented key prefix as a contract.
PK/AKis an observed Alpaca convention, not a documented one, and most venues have no prefix at all. Gate on the endpoint; corroborate with the prefix. - Echoing the full URL into the exception. A signed Binance URL carries
&signature=<hmac>; an OAuth callback carries the token. A leak-prevention guard that writes those into logs and tracebacks has become the leak. - Calling the HTTP client directly. One code path that skips
validate_request_boundary()— a health check, a retry helper, a vendored SDK — is the path the bad request will take.
Verification
CredentialEnvironmentGuard(SANDBOX)withPK…tohttps://paper-api.alpaca.markets/v2/orders$\implies$ returnsTrue.- Same guard to
https://api.alpaca.markets/v2/orders?client_tag=paper$\implies$ raisesSecurityViolationError("ENDPOINT LEAK DETECTED…"). This is the regression case: the previous substring implementation approved it. CredentialEnvironmentGuard(PRODUCTION)withAK…tohttps://api.alpaca.markets.attacker.example/v2/orders$\implies$ raisesSecurityViolationError, message containingnot a recognised.CredentialEnvironmentGuard(PRODUCTION)with aPK…key $\implies$ raisesSecurityViolationError("CREDENTIAL LEAK DETECTED…").- Any unregistered broker $\implies$ raises
SecurityViolationErrorunlessallow_unknown_brokers=Truewas passed explicitly. - A URL carrying
&signature=<secret>$\implies$ the secret does not appear in the raised message. - Run
python -m unittest discover -s skills/sandbox-credential-leakage-prevention/scripts(46 tests).