Are API subdomain credential boundaries actually enforced?
API subdomain credential boundaries prevent tokens from reaching sibling, regional, vanity, and tenant hosts through redirects or loose client code.

A hostname is not an ownership hint. It is part of the destination identity that decides where an API credential goes. If a client holds one bearer token and treats api.example.com, files.example.com, api.eu.example.com, and customer.example.com as morally equivalent because they share a suffix, the client has already made the dangerous decision.
This mistake hides behind friendly architecture. A company may own every name under a parent domain. DNS may send several names to the same load balancer. Certificates may cover them all. None of that says a credential issued for one API should reach another hostname. Credentials need an explicit destination rule, and that rule needs tests that fail when somebody broadens it later.
Hostnames do not inherit trust from a parent domain
api.example.com and admin.example.com may share a registrable domain, but they are separate origins. An origin includes scheme, host, and port. https://api.example.com and https://api.example.com:8443 are separate origins too. The distinction matters because a client chooses a network destination by that authority, not by the marketing relationship between services.
RFC 9110 defines an HTTP authentication protection space using the origin and, when present, a realm. It also warns that relying on realms alone can expose credentials to other resources on an origin, and recommends separate hostnames or ports where different parties need separation. That is a useful warning, but it is not permission to treat all names below one suffix as a single protection space.
Engineers often blur three different ideas:
- A parent domain names a DNS namespace.
- A site groups related web origins for some browser security rules.
- An origin identifies one scheme, host, and port for an HTTP destination.
Only the first two make subdomains look related. Your API credential dispatcher should use the third.
This matters even when the credential issuer puts a broad aud claim into a token. A broad audience claim tells the receiving service what it may accept. It does not instruct your client to offer the token to every endpoint that might accept it. The sender still owns the duty to minimize distribution.
I have seen this fail in a predictable way. A team starts with a single endpoint, adds api.staging.example.com, and keeps a helper called getApiKey(). Six months later the helper sits underneath five services. It has no host argument, no allowlist, and no idea why it should refuse a call. The key did not become unsafe because someone used an exotic exploit. It became unsafe because the code stopped carrying the fact that the key belonged to one destination.
Make the host binding visible in configuration and in code review. A credential selector that accepts only a credential name is already missing half of its input.
The four hostname shapes fail in different ways
Sibling, vanity, regional, and customer-specific domains deserve separate test cases because they fail for different reasons. One vague assertion that a token stays within "our domains" will miss at least one of them.
A sibling host sits next to the intended host: metrics.example.com instead of api.example.com. This often exposes a shared ingress, an old service, or an internal tool that was never meant to see production credentials. A sibling is especially dangerous when a generic proxy forwards headers unchanged to an upstream chosen by route configuration.
A vanity host is a friendly alias such as api.brand.example or developer.example.com. Providers introduce these names during product renames, acquisitions, or migrations. The alias may terminate at a different edge, use different telemetry, or redirect to a canonical host. Do not grant it credential access based on a redirect you saw once in a browser.
A regional host changes location and often changes service ownership: api.us.example.com, api.eu.example.com, or api.ap-southeast.example.com. The request may carry business data before the application rejects the token. A 401 response does not prove that sending the credential and body to the region was acceptable.
A customer-specific host makes multi-tenant mistakes more severe: acme.vendor.example and northwind.vendor.example might resolve through the same infrastructure, yet each hostname represents a tenant boundary. A token broad enough to work at both may be intentional for a central control plane. It should never become the default credential for tenant-directed requests.
Write each category into your inventory. Do not collapse them into a field called allowed_domains and assume a wildcard makes the policy easier. The wildcard moves the hard decision out of sight.
Bind the credential selection to the complete authority
A safe client maps a request authority to one credential record, then rejects the request when no record matches. The complete authority means scheme, normalized hostname, and effective port. For an ordinary HTTPS API on port 443, the visible configuration can stay simple, but the implementation must still reject an unexpected port rather than silently reusing the token.
A workable inventory looks like this:
credentials:
billing-production:
allowed:
- https://api.billing.example.com:443
header: Authorization
scheme: Bearer
telemetry-eu:
allowed:
- https://ingest.eu.example.net:443
header: X-Write-Key
The point is not YAML. The point is that the token name does not stand alone. billing-production has one explicit destination, and telemetry-eu cannot leak into a U.S. endpoint because a caller changed only a hostname string.
Avoid this pattern:
const headers = {
Authorization: `Bearer ${process.env.PRODUCTION_API_TOKEN}`
};
await fetch(userSuppliedUrl, { headers });
The code has coupled an unrestricted destination to a high-privilege credential. Developers sometimes defend it by saying the URL comes from a trusted configuration file. That file is still an input boundary. Deployment tooling, environment overrides, pull requests, feature flags, and a compromised build job can all alter it.
Use a request builder that refuses to construct authenticated requests until it has matched a normalized destination. Keep hostname normalization boring and strict:
- Parse the URL with a real URL parser, not string suffix checks.
- Require
https:unless a documented local development exception exists. - Lowercase and canonicalize the hostname through the parser.
- Compare the resulting scheme, host, and port with exact approved authorities.
- Add the authentication header only after the match succeeds.
Do not write host.endsWith("example.com"). It accepts notexample.com. Do not repair it with endsWith(".example.com") and call the job finished. That expression still gives every present and future subdomain the token, including names delegated to a customer, a vendor, or a forgotten development environment.
The distinction between destination authorization and credential authorization needs to stay sharp. A receiving API can reject an out-of-scope token. Your sender should prevent the token from leaving for that API in the first place. The first control limits exposure. The second decides access. You need both.
A redirect is a new destination, not a continuation of the old request
Redirect handling creates a second credential boundary. The first host may be approved, then return a Location header pointing to a vanity name, a regional endpoint, object storage, or a host an attacker controls through an open redirect. If the client follows automatically, it must re-run destination authorization before sending any credential or request body.
libcurl makes the difference unusually clear. By default, it does not send internally generated authentication or explicitly set cookie headers to a different host during redirects. Its CURLOPT_UNRESTRICTED_AUTH option changes that behavior and may send credentials to hosts selected by redirect responses. The curl project warns that custom headers require separate care, because the library cannot infer which arbitrary headers carry secrets.
That last detail catches experienced teams. They test Basic authentication with a standard client and see a cross-host redirect behave safely. Then their production integration uses X-Api-Key, Authorization: Bearer, or X-Signature inserted as a generic header. A library can preserve that header unless the application removes it. A passing test for one authentication mechanism says nothing about another.
Treat redirects by request class:
- For write operations, reject redirects unless the API contract requires them.
- For read operations, inspect each redirect target and rebuild headers from the target's approved credential record.
- For signed requests, rebuild the signature after authorization of the final destination. Never forward a signature created for the first host.
- For uploads, do not forward the original authorization header to a pre-signed storage URL. The query signature or form fields already carry the limited authority that storage endpoint expects.
A redirect test needs to inspect the request that arrives at the second server. Checking only the final status code is not enough. A 200 response from a harmless test receiver can hide the fact that it received the production header you meant to protect.
Retries need the same discipline. Some HTTP wrappers reconstruct requests from a cached header map. If a retry follows service discovery to a new authority, discard the cached map and ask the credential selector again. Reusing headers is faster. It is also how destination context disappears.
Build a negative test rig before you trust the allowlist
The test that matters is negative: a credential must appear at the intended host and must not appear at every plausible wrong host. You can run this with two local HTTPS test servers, but the receiver must record only the presence and name of sensitive headers. Do not put real values into test logs.
For a shell-level check, map harmless names to local servers with curl's --resolve option and use a disposable token. Start one receiver on port 8443 for the approved host and another on 9443 for the sibling. Each receiver should output a record shaped like this:
host=api.test.example
path=/v1/ping
authorization=present
x-api-key=absent
The sibling receiver should output the inverse result:
host=metrics.test.example
path=/v1/ping
authorization=absent
x-api-key=absent
Then exercise the actual HTTP client wrapper, not a clean-room version of its logic. A curl probe can expose the basic wiring:
curl --silent --show-error \
--resolve api.test.example:8443:127.0.0.1 \
--header 'Authorization: Bearer test-token-do-not-use' \
https://api.test.example:8443/v1/ping
That command intentionally places the header on the request, so it only proves what the receiver logs. It does not prove your application selects credentials safely. Your application test should call its normal request() function with the same approved authority, then repeat with each wrong authority and assert that it raises an error before opening a connection.
Use a matrix that forces the decisions people usually assume away:
| Requested authority | Expected credential behavior |
|---|---|
https://api.test.example | Send the intended test credential |
https://metrics.test.example | Reject before sending |
https://api.eu.test.example | Reject unless separately configured |
https://tenant-a.test.example | Reject unless tenant binding is explicit |
| approved host redirecting to sibling | Follow only after reauthorization, normally without the original credential |
Do not use an external request-bin service for this test. You would teach your team to send test secrets to a third party while checking whether they send secrets to third parties. A local receiver is trivial and keeps evidence under your control.
Put the matrix into continuous integration. A unit test on isAllowedHost() helps, but an integration test catches the common regression: somebody adds a default header to a lower HTTP layer after the host check already ran.
Browser rules are not agent rules
Browser vocabulary causes bad assumptions in agent and backend code. "Same site" can include subdomains, while "same origin" does not. MDN uses https://example.org and https://login.example.org as an example of two origins that are same-site. That distinction exists because a compromised subdomain can attack a sibling through same-site paths.
Fetch defaults to credentials: "same-origin", which prevents browser fetch from automatically including credentials on cross-origin requests. A developer may see that default, test a frontend call, and conclude a bearer token cannot move from one subdomain to another. That conclusion does not survive contact with server-side code. A backend fetch wrapper can attach whatever header its author gives it. A CLI can do the same. An autonomous agent can invoke a generic HTTP tool with a URL and headers unless the tool keeps the credential outside the agent's reach.
CORS does not repair this. CORS mainly controls whether browser JavaScript can read a response. It does not turn a broad server-side header injector into a safe credential dispatcher. In some browser requests, the browser can send the credential and then refuse to expose the response to script. That is not an acceptable data-loss prevention control.
Cookies add another source of confusion. Cookie domain attributes may allow a cookie to reach subdomains, while host-only cookies do not. Bearer headers have no comparable built-in domain scope. If your client attaches Authorization, it has made an explicit decision for that request. Do not borrow cookie mental models for API keys.
Customer domains need an issuer boundary, not a naming convention
A central API can legitimately call many customer endpoints, but it needs credentials that encode why the call may cross tenant boundaries. The safest setup gives each customer host a separate credential record with a narrow scope. The next best setup uses a short-lived token whose audience and tenant claims the destination verifies, with a client allowlist that still names each permissible host.
A global token with a broad role is popular because onboarding becomes easy. Add a tenant, point the integration at its subdomain, and the call works. The same convenience makes a typo, a malicious configuration change, or a confused agent capable of reaching another customer's service with a credential that has no meaningful destination limit.
Do not solve this by creating *.customers.example.com as an approved pattern and telling yourself that every match is a customer. Ask who can create those names, who can delegate DNS, which hosts route to preview environments, and whether names persist after customer offboarding. A wildcard makes all those questions security decisions, usually without leaving a review record.
There are cases where a controlled wildcard is appropriate. A provider may issue per-tenant tokens where the token includes a tenant identifier, the service rejects mismatches, and an internal registry verifies the tenant host before the client sends. In that design, the wildcard is not the authorization rule. It is a narrow convenience sitting behind an authoritative tenant registry. If you cannot name that registry and test its failure behavior, use exact entries.
Private DNS creates the same issue inside a company. payments.prod.internal and payments.dev.internal may not be public names, but they are different destinations with different operational controls. Internal DNS is not a substitute for credential scoping.
Host checks must happen before discovery and after normalization
Service discovery, custom routing, and proxy configuration can quietly bypass an otherwise sensible allowlist. If code checks the original URL, then a resolver substitutes an internal target, the application may send a credential to a different authority. Conversely, a check that uses a mutable Host header can approve a request whose network connection goes elsewhere.
Use the parsed URL authority as the policy input. Establish the TLS connection for that authority and verify the certificate normally. Do not disable certificate verification to make an internal route or test fixture work. curl's own security guidance says a client that cannot authenticate the peer cannot know it reached the intended server.
Then decide what your trust model says about proxies. A forward proxy is a transport choice, not a new credential destination, if the client establishes TLS to the approved origin through it. A reverse proxy that terminates TLS is part of the service boundary and needs the same scrutiny as the API itself. An HTTP proxy that receives plaintext authorization headers has access to the credentials. Do not call that detail "just infrastructure."
Normalize internationalized hostnames through a standards-compliant URL implementation, and compare the canonical result. Reject userinfo in URLs such as https://[email protected]/; credentials in URLs leak into logs, histories, and debugging output. Reject fragments for HTTP requests and decide explicitly whether query strings may contain pre-signed credentials. A generic sanitization function cannot rescue a design that accepts every URL and hopes to identify bad ones later.
The important order is simple: parse, normalize, authorize destination, select credential, construct headers, connect. If any later operation changes the authority, restart at destination authorization.
Audit the decision, not the secret
An audit record should prove what the client considered the destination and which credential rule it selected, without recording the secret. You need this record when someone asks whether a token could have reached a sibling host after a deployment change.
Store fields such as request ID, time, normalized authority, credential record ID, authorization outcome, redirect source and target, and an outcome code. Hash or redact paths when paths contain customer identifiers. Do not log Authorization, custom key headers, query strings holding signatures, or complete request bodies just because they help debugging.
A good audit entry answers a concrete question:
request_id=01J...
authority=https://api.billing.example.com:443
credential=billing-production
destination_check=allowed
redirect_count=0
result=201
For a rejected sibling request, the record should show credential=none and destination_check=denied. That distinction proves the client refused before it selected a secret. If the log instead says a credential name and then reports a 403 from the wrong host, the system already sent more than it should have.
Sallyport keeps the secret in its encrypted vault and evaluates an action before the agent receives any credential material. Its Activity journal and Sessions journal give teams a way to inspect the action path and revoke a running agent session when a host-bound request goes wrong.
Do the inventory first. For every credential, write one intended authority and then name one sibling, one vanity or migration name, one regional name, and one customer-specific name that must not receive it. If your client cannot make those negative assertions today, it does not have a credential boundary. It has a hopeful convention.
FAQ
Do API keys automatically work across subdomains?
No. api.example.com and billing.example.com are different origins, even when the same team operates both names. Treat a credential binding as exact unless its issuer explicitly documents a broader audience and you have tested that scope.
Is it safe to send one bearer token to all subdomains?
They should only receive the token if your client is deliberately configured to send it there. A shared parent domain proves ownership of a namespace, not authorization to reuse one service's bearer credential.
Can an HTTP redirect leak an API credential to another host?
A redirect can change the destination after the first request, so redirect handling needs its own host check. Never assume an HTTP client strips a custom credential header just because it strips built-in HTTP authentication.
Should every customer subdomain use a separate credential?
No. A host like tenant-a.api.example.com should normally have its own tenant-bound credential or a token with a tenant claim that the service enforces. A general admin key pointed at tenant hosts turns a routing error into a cross-tenant incident.
Do regional API endpoints need separate credentials?
Yes, if the regional endpoint has a different hostname. The fact that two regional services belong to one provider does not make the names interchangeable, and region mistakes often send data to the wrong residency boundary before authentication even fails.
Are vanity API domains safe for production credentials?
Vanity names are risky because they look harmless and often sit in front of different routing, logging, or ownership arrangements. Bind credentials to the canonical service host unless the provider documents the vanity name as an equivalent authenticated audience.
How do I test whether my client sends a token to sibling domains?
Record the exact URL, the credential selected, redirect behavior, and whether the request carried an authentication header. The most useful test runs a harmless endpoint against approved and deliberately unapproved hosts, then fails if the credential appears anywhere it should not.
Does CORS stop credentials from reaching a subdomain?
CORS governs what browser JavaScript can read, and cookies have their own domain and SameSite rules. A server-side agent, CLI, or custom HTTP client can send any header to any host unless its code stops that request.
Should I use wildcard host allowlists for API credentials?
Exact host allowlists are usually right for bearer tokens, custom API-key headers, and SSH destinations. Use a narrow wildcard only when the credential system itself proves the selected tenant or service audience and you can continuously verify which concrete hosts matched.
What is the first fix for a credential boundary mistake?
The first fix is to inventory every credential-to-host binding and delete broad defaults such as *.example.com. Then add a negative test for each credential: one approved destination must receive it, while sibling, vanity, regional, and tenant destinations must not.