The API security decision record: why the authentication enforcement boundary you chose determines your authorization bypass surface and your credential stuffing exposure
API security decisions are made in three founding sessions that never document the operational consequences — the authentication enforcement boundary session that centralizes JWT validation at the API gateway without specifying that any endpoint added to the application server bypasses the gateway auth layer (a 27-person SaaS adds a webhook receiver endpoint directly to the application server 6 months later to reduce latency; the endpoint accepts a caller-supplied integer tenant_id parameter and has no application-layer authentication check; a security researcher discovers the unprotected endpoint and reads the webhook payloads of any tenant in the system by iterating through integer IDs from 1 to the current maximum; the founding session documented "JWT validation at the API gateway" without specifying that this is a minimum enforcement point requiring defense-in-depth at the application layer, that any endpoint handling tenant data added outside the gateway routing path requires its own auth check, or that caller-supplied tenant identifiers must be validated against the authenticated session's authorized tenant regardless of how they arrive); the rate limiting policy session that sets per-IP capacity limits without specifying an endpoint-specific account-level policy for the login endpoint (a 33-person startup sets API rate limits at 100 req/min per IP as a capacity protection measure and treats the login endpoint identically to all other endpoints; 18 months later the company has 9,400 users with email/password authentication; an attacker obtains a credential list from a prior public breach and conducts credential stuffing using 1,840 rotating residential proxy IPs, each sending 25 attempts per rotation cycle at approximately 1.7 req/min — well under the per-IP threshold; over 72 hours the attacker tests 2.3 million email/password pairs; 23 customer accounts are compromised; the founding session documented "rate limit: 100 req/min per IP" without specifying that the login endpoint requires an account-level lockout policy that counts failed authentication attempts against the target email address regardless of source IP, that credential stuffing attacks are structurally immune to per-IP thresholds when residential proxy pools rotate at sub-threshold rates, or that bot scoring signals from the CDN layer are available but not wired to any authentication policy); and the input validation session that validates URL syntax and DNS resolution but not destination address space (a 41-person SaaS builds webhook delivery functionality where users configure a callback_url for their account; the endpoint validates that the URL is syntactically valid and that the hostname resolves in DNS; no validation checks whether the resolved IP address falls in RFC-1918 private space, loopback space, or the link-local range 169.254.0.0/16 that includes the AWS instance metadata service; a security researcher submits http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role as the callback URL in the webhook delivery test endpoint; the application fetches the URL from the application server, receives the EC2 IAM role credentials in the response, and returns them to the caller as part of the "webhook test result" JSON body; the EC2 role has read access to S3 buckets containing customer data exports; the founding session documented "validate that the webhook URL is a valid HTTP/HTTPS URL" without specifying that URL-type inputs that trigger server-side fetches require allowlist validation against permitted destination address ranges, that the AWS instance metadata endpoint at 169.254.169.254 is a prohibited destination category, or that SSRF is a distinct input validation concern requiring a dedicated policy separate from field-type validation). What none of these sessions produce is the application-layer authentication contract that the gateway cannot enforce on its own, the account-level lockout model that per-IP rate limits cannot substitute for, or the URL destination allowlist that syntactic URL validation cannot replace.
A 27-person B2B SaaS company deployed an API gateway in month four of development to handle cross-cutting concerns at a single chokepoint: JWT validation, request logging, rate limiting, and TLS termination. The architecture was sound — centralizing these concerns at the gateway meant that application developers writing new endpoints could focus on business logic without implementing authentication in each handler. The JWT validation policy was documented in the engineering wiki: "All API requests must carry a valid JWT issued by our auth service. The gateway validates the JWT signature and expiry before forwarding the request to the application server." Over the next six months, the gateway validated every incoming request and the application server operated under the assumption that any request reaching it had been authenticated.
In month ten, a developer working on a webhook integration feature needed to add a webhook receiver endpoint — an endpoint that accepted POST requests from third-party services delivering webhook payloads for tenant accounts. The endpoint had a different latency requirement than other endpoints: webhook delivery services impose short timeout windows (typically 5 seconds), and the API gateway's processing chain — JWT validation, rate limit check, request logging, forwarding — added 18 milliseconds of latency that occasionally pushed responses over the webhook service's timeout when the application server was under load. The developer's solution was to add the webhook receiver endpoint directly to the application server's routing table, bypassing the gateway. The Nginx configuration on the application server was updated to add a location /api/webhooks/receive block that proxied directly to the application's local port, bypassing the API gateway reverse-proxy path that all other routes used. The developer documented the bypass in a comment in the Nginx config: "# Direct path to app server — bypasses gateway for lower latency on webhook delivery."
The webhook receiver endpoint accepted POST requests with a JSON body that included a tenant_id field identifying which tenant account the webhook payload was for. The endpoint looked up the tenant's webhook configuration, validated the incoming payload signature against the tenant's registered webhook secret, stored the payload in the tenant's event log, and returned a 200 OK. The tenant_id field was supplied by the caller — the third-party webhook delivery service included it because that was how the tenant had configured their integration. The developer's mental model for the endpoint's security was: "Only our integration partners send requests to this endpoint, and they only send payloads for the tenants they serve, and the payload signature validates that the payload came from the legitimate source." This model was correct for the intended call path. It did not account for the fact that the endpoint was publicly reachable by anyone with a network connection, that no authentication check verified the caller's identity before processing the request, and that the tenant_id parameter accepted any integer value regardless of the caller's authorized scope.
Nine months after the endpoint was added, a security researcher conducting a routine reconnaissance of the company's API endpoints noticed that the /api/webhooks/receive path returned a different response from the gateway than other paths — specifically, it returned the application server's response directly, without the gateway's standard headers. The researcher sent a GET request (not a POST) to the endpoint and received a 405 Method Not Allowed response from the application — which confirmed the endpoint was handled by the application layer. Sending a POST request with a JSON body {"tenant_id": 1, "event_type": "test"} returned a 400 response: "Missing required field: payload." Adding a dummy payload field returned a 400: "Invalid payload signature." The researcher modified the request to include a valid-looking but incorrect signature and received a 400 with a different error: "Signature validation failed for tenant 1." The error messages confirmed that the endpoint was looking up tenant 1's configuration without first verifying the caller's authorization to access tenant 1's configuration — it was processing the tenant_id parameter and returning information about the lookup result before performing the signature check. The researcher iterated through tenant IDs from 1 to 500 and found that tenants with IDs in the range 1–342 returned either "Signature validation failed for tenant N" (indicating an active webhook configuration existed) or "No webhook configuration found for tenant N" (indicating no configuration). This enumerated the presence or absence of webhook configurations for 342 tenants. Further, tenants with active configurations returned an error that included the configured webhook secret prefix: "Signature validation failed for tenant 12: expected signature starting with 'abc123'" — a format string in the error handler that inadvertently exposed part of the secret. The researcher filed a responsible disclosure report.
The vulnerability was not in the JWT validation logic — the gateway's JWT validation was correct. The vulnerability was in the implicit assumption that the gateway's authentication was the exclusive enforcement point. The application server's code contained no authentication middleware; every handler on the application server assumed that any request reaching it had been authenticated by the gateway. For 98% of the application's routes, this assumption was correct because those routes were reachable only through the gateway proxy. For the webhook receiver endpoint, the assumption was incorrect because the route was accessible directly. The remediation required adding application-layer authentication middleware as a second enforcement point, removing the caller-supplied tenant_id parameter from the webhook receiver (and instead deriving the tenant from the authenticated session's claims), and auditing all Nginx configuration blocks for direct-to-application routes that bypassed the gateway. The audit found two additional direct routes added for similar operational reasons — neither handled sensitive data, but neither was documented in the endpoint inventory, and both were added after the authentication enforcement policy was written, by developers who did not know the policy applied to direct routes as well as gateway routes. The founding session that established "JWT validation at the API gateway" documented the gateway setup and the JWT library configuration. It did not specify that application-layer authentication middleware was required as a defense-in-depth layer, that caller-supplied tenant or user identifiers must be validated against the authenticated session's authorized scope, or that any route added outside the gateway routing path must be registered in the endpoint inventory and reviewed for authentication coverage.
A 33-person startup built their primary API with a standard rate limiting configuration: 100 requests per minute per IP address globally, 200 requests per minute per authenticated user. The configuration was designed for capacity protection — preventing any single caller from monopolizing server resources — and was applied uniformly across all API endpoints including the authentication endpoint (POST /api/auth/login). The rate limit choice was pragmatic and well-reasoned for its stated purpose: 100 req/min was 10x the expected peak legitimate usage for any single client, and the per-user limit was set higher to accommodate mobile clients on networks with shared NAT IPs. The configuration was documented in the infrastructure runbook as "API rate limiting: 100 req/min per IP, 200 req/min per authenticated user."
Eighteen months after the startup launched, its customer base had grown to 9,400 users. All users authenticated with email and password — the company had considered adding a magic-link option but had not yet prioritized the work. The user table held 9,400 email addresses, most with passwords hashed using bcrypt at cost factor 12. The startup's email domain was clearly identifiable from the company's public website.
An attacker purchased access to a residential proxy pool service offering 2.3 million rotating IPs from real home internet connections across 40 countries. Residential proxy IPs are indistinguishable from legitimate user IPs at the infrastructure layer — they carry real residential ISP ASN assignments and have no history in threat intelligence feeds because they are real end-user devices enrolled in the proxy network without the device owner's knowledge. The attacker obtained a credential list from a public breach dataset — 18 million email/password pairs from a 2019 database breach of an unrelated social network — and cross-referenced the email domains against the startup's known customer domains (identifiable from the company's case study pages and LinkedIn profiles of users who had listed the product in their work history). The cross-reference identified approximately 800 email addresses likely associated with the startup's customer base that appeared in the breach dataset.
The attacker configured the credential stuffing tool to use 1,840 rotating IPs (a subset of the residential proxy pool), with each IP sending no more than 25 requests before rotating to the next IP. At 25 requests per IP rotation, each IP sent requests at approximately 1.7 requests per minute over a 15-minute rotation window — 60 times under the 100 req/min rate limit threshold. The tool ran for 72 hours, cycling through the 1,840 IPs and testing each of the 800 target email addresses against multiple password variants from the breach dataset. The startup's authentication endpoint imposed no delay between failed attempts, no CAPTCHA challenge after repeated failures for the same account, and no account lockout after N consecutive failures. From the attacker's perspective, the login endpoint behaved identically for successful and failed authentication attempts in terms of rate limiting — both were permitted under the per-IP threshold.
Across the 72-hour attack window, 23 customer accounts were successfully authenticated by the stuffing tool — accounts whose users had reused the same password from the breached social network, or had used a simple variation (appending a year, capitalizing the first letter) that the breach dataset's common variation list included. The attacker extracted session tokens for each of the 23 compromised accounts and, before the accounts' legitimate owners noticed unauthorized login alerts (which the startup did not send), accessed the accounts' data exports, copied billing history, and in two cases sent messages through the accounts' connected integrations. The startup discovered the compromise 3 days later when two affected users reported that their accounts had sent messages they had not authorized. An investigation revealed the attack pattern in the access logs: 1,840 source IPs, each contributing a small number of requests to the /api/auth/login endpoint over 72 hours, with no single IP crossing the rate limit threshold. The CDN logs showed that the requests carried uniform User-Agent strings (all identical — a common credential stuffing tool's default) and had request timing distributions inconsistent with human interaction (requests arrived at uniform intervals rather than with the variable timing of human typing). These signals had been available in the CDN logs throughout the attack but were not monitored.
The remediation required three changes: account-level lockout after 5 failed authentication attempts for the same email address within any 15-minute window (regardless of source IP); a CAPTCHA challenge triggered after the second failed attempt for any given email; and a bot scoring pipeline wired from the CDN's bot detection signals to the authentication service, blocking requests scored above a bot-probability threshold before they reached the login endpoint. The rate limiting configuration — 100 req/min per IP — remained unchanged because it was correct for its purpose (capacity protection) and had never been intended to prevent distributed authentication attacks. The founding session that established the rate limiting policy documented the per-IP and per-user thresholds and the middleware configuration. It did not specify that the login endpoint requires a distinct security-oriented rate limit separate from the capacity-oriented global limit, that credential stuffing attacks operate by distributing requests across many IPs to stay under per-IP thresholds (making per-IP limits ineffective as a credential stuffing control), that the login endpoint must implement account-level failed-attempt counting regardless of source IP, or that bot scoring signals from the CDN are available and should be wired to authentication policy decisions.
A 41-person SaaS company built a webhook delivery system in year two of their product's development. Users could configure a callback_url for their account, and the platform would POST event notifications to that URL whenever a relevant event occurred in the user's account. The webhook configuration endpoint accepted a PUT request at /api/settings/webhook with a JSON body containing the callback URL. A separate endpoint, POST /api/settings/webhook/test, allowed users to trigger a test delivery to their configured URL to verify that the endpoint was reachable and responding correctly. The test endpoint was a convenience feature requested by users who had trouble debugging their webhook receiver setup.
The URL validation logic was written by the developer who implemented the feature. The validation checked two things: that the submitted URL matched a regular expression for valid HTTP or HTTPS URLs (scheme, hostname, optional port, optional path), and that the hostname resolved successfully in DNS. The developer's mental model for webhook security was that users would configure URLs pointing to their own servers, and that the two validations ensured the URL was technically well-formed and reachable. The developer did not consider that the server performing the DNS resolution and the fetch was the application server running in the company's AWS VPC, which had outbound network access to the internet and, through the VPC's routing table, to the AWS instance metadata service at the non-routable link-local address 169.254.169.254.
Two years after the webhook feature shipped, the company had grown to 41 engineers and had expanded its infrastructure to include six EC2 instance types running different services. The IAM role assigned to the instances running the webhook delivery service had S3 read permissions scoped to the data export bucket (used for generating customer data exports on demand) and SQS send permissions for the event delivery queue. The S3 bucket contained export archives for all customer accounts going back 18 months.
A security researcher participating in the company's bug bounty program configured a webhook URL in their test account and used the test delivery endpoint to verify it was working. While reviewing the test endpoint's behavior, the researcher submitted the AWS instance metadata URL as the callback URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role. The hostname 169.254.169.254 passed the DNS resolution check — the validator attempted to resolve it, received a response (because the AWS VPC routes the link-local address to the metadata service, and the DNS resolver could resolve the hostname as an IP even without a DNS lookup because it was an IP address literal), and marked it as valid. The URL passed the HTTPS scheme check — the validator accepted HTTP URLs as valid. The webhook delivery test endpoint fetched the URL using the application server's HTTP client, which sent a GET request to 169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role from within the VPC. The instance metadata service responded with a JSON document containing the temporary AWS credentials for the EC2 IAM role: AccessKeyId, SecretAccessKey, Token, and Expiration. The application's test endpoint received the metadata service's JSON response as the webhook delivery response body and returned it to the researcher as part of the test result: {"status": "delivered", "response_code": 200, "response_body": "{\"Code\": \"Success\", \"AccessKeyId\": \"ASIA...\", \"SecretAccessKey\": \"...\", \"Token\": \"...\", \"Expiration\": \"2026-05-21T14:23:00Z\"}"}.
The researcher filed a critical severity bug report with the response body included as evidence. The company revoked the IAM role credentials and rotated the role within two hours of receiving the report. The S3 bucket access logs showed no unauthorized access during the window between the credential issuance and revocation. The remediation required rewriting the URL validation to include a third step after DNS resolution: check the resolved IP address against a prohibited range list (RFC-1918, loopback, link-local including 169.254.0.0/16, and the company's internal VPC CIDR range). The fix also changed the HTTP client used for webhook fetches to disable automatic redirect following (preventing SSRF via redirect chains), set an explicit connection timeout (preventing SSRF via slow-response attacks that hold connections open), and added a Content-Type check on the response (preventing the application from logging or returning unexpected response types). The founding session that designed the webhook feature documented the callback URL field, the DNS resolution validation, and the test delivery endpoint. It did not specify that URL-type inputs that trigger server-side fetches require validation against prohibited destination address ranges, that the AWS instance metadata endpoint is reachable from any EC2 instance and is a standard SSRF target, that link-local IP addresses (169.254.0.0/16) are not excluded by DNS resolution checks because they are routable within AWS VPCs, or that SSRF is a distinct category of vulnerability requiring a dedicated input validation policy separate from URL format validation.
Structural properties set by the API security decision
Three structural properties are determined when a team makes their early API security decisions. None appear explicitly in the session that establishes gateway-based authentication, the session that configures rate limiting, or the session that adds URL-accepting input fields — they are the operational consequences of security choices made under correct but incomplete assumptions about the threat model at the time the decisions are made.
Property 1: The authentication enforcement boundary and the authorization bypass surface. An API gateway that centralizes authentication creates an enforcement boundary — the set of all ingress paths that route through the gateway and are therefore subject to the gateway's authentication checks. The authorization bypass surface is the complement of that boundary: the set of all application endpoints that are reachable by paths that do not route through the gateway. When the gateway is established as the exclusive authentication enforcement point and application code contains no secondary enforcement, the bypass surface contains every endpoint that a developer might later add outside the gateway routing path for any operational reason (latency optimization, traffic separation, webhook delivery, health checks, internal service endpoints). The bypass surface is zero when authentication is enforced at both the gateway layer and the application layer — when every request handler in the application verifies the authentication token before processing the request, regardless of the route by which the request arrived. Gateway authentication and application-layer authentication are not redundant; they are different enforcement points with different blast radiuses. If the gateway's authentication fails (misconfiguration, software bug, deliberate bypass), application-layer authentication is the backstop. If an endpoint bypasses the gateway, application-layer authentication is the only enforcement point. The structural requirement is defense-in-depth: the application's authentication middleware must be applied to every request handler as a framework-level default (opt-out for explicitly marked public endpoints) rather than as a gateway-level-only policy (opt-in for endpoints that use the gateway path). The second element of the bypass surface is caller-supplied identity parameters: any endpoint that accepts a caller-supplied tenant_id, user_id, account_id, or similar parameter that determines whose data is accessed has an implicit authorization check requirement — the authenticated session's authorized scope must be verified to include the requested identity before the parameter is used to query data. A caller-supplied identity parameter without an authorization check is a horizontal privilege escalation surface: any authenticated caller can request data for any identity by substituting a different integer or UUID. This is distinct from authentication (verifying who the caller is) and must be enforced by the application handler regardless of what the gateway does. The authentication strategy decision record documents the JWT claim structure and the session model — the claims that identify the authenticated caller's tenant and role scope, which are the inputs to the per-handler authorization check that validates caller-supplied identity parameters. The authorization model decision record documents the authorization policy for cross-tenant access — the cases where one authenticated entity is permitted to access another entity's resources, the conditions under which that access is permitted, and the enforcement mechanism (application-layer policy check, not gateway-layer rule).
Property 2: The rate limiting policy and the credential stuffing exposure surface. API rate limiting serves two distinct purposes that require different policy designs: capacity protection (preventing any caller from consuming a disproportionate share of server resources) and authentication abuse prevention (preventing automated attacks against authentication endpoints). Per-IP rate limiting is well-suited to capacity protection — it distributes the capacity fairly among callers and prevents any single source from saturating the server. Per-IP rate limiting is structurally incapable of preventing credential stuffing: the attack works by distributing requests across many IPs, each staying below the per-IP threshold, so that the combined attack volume is limited only by the size of the proxy pool and not by any per-IP control. The credential stuffing exposure surface is the set of user accounts whose passwords are predictable via breach dataset lookups and who have no account-level lockout that triggers on a small number of failed attempts. The surface cannot be closed by per-IP rate limiting alone. It requires an account-level control — a failed-attempt counter keyed by target email address (not by source IP) that triggers a lockout or challenge after N failed attempts regardless of how many different source IPs submitted those attempts. The lockout counter resets on successful authentication (so that a user who misremembers their password is not permanently locked out) but does not reset on time alone without a verified reset action (email verification or password reset) — because the attacker can simply wait out a time-based lockout window and resume. The second element of the exposure surface is bot signal utilization: CDN and load-balancer layers emit signals that distinguish automated clients from browser-originated clients — User-Agent consistency (bots often use a single static User-Agent string; human browsers vary across versions and platforms), request timing distributions (bot requests arrive at near-uniform intervals; human form submissions have variable timing reflecting typing and reading behavior), header completeness (bot HTTP clients often omit Accept-Language, Accept-Encoding, or other browser-standard headers), and ASN classification (datacenter ASNs and known proxy services are highly correlated with automated traffic). These signals are available in CDN access logs and via CDN bot-score APIs but must be explicitly wired to authentication policy decisions — they are not automatically applied to rate limiting or lockout policies. The API rate limiting decision record documents the capacity-oriented rate limiting configuration — the per-IP and per-user thresholds and the middleware configuration. The API security decision record supplements it by specifying the authentication-specific controls that operate on different axes than capacity rate limits, and by establishing the explicit responsibility boundary: capacity rate limiting is a capacity decision, and authentication abuse prevention is a security decision, and each requires its own policy designed for its own threat model.
Property 3: The input validation surface and the SSRF escalation path. Every API endpoint that accepts a URL-type input and uses that URL to trigger a server-side network request has an implicit SSRF surface — the set of destination addresses that the server can reach but that the API's callers are not authorized to access. The surface includes all private network resources reachable from the server: cloud provider instance metadata services (169.254.169.254 in AWS/GCP/Azure), RFC-1918 internal services (internal databases, cache servers, internal APIs, Kubernetes cluster services), loopback services (services bound to 127.0.0.1 accessible only on the server's local network stack), and any other network resource accessible from the server's network interface that is not intended to be publicly accessible. The SSRF surface is zero for endpoints that do not perform server-side fetches. It grows to include every reachable internal address for endpoints that fetch caller-supplied URLs without validating the destination address against an allowlist of permitted external ranges. Syntactic URL validation (checking that the URL matches an HTTP/HTTPS URL pattern) does not close the SSRF surface because private and link-local IP addresses are syntactically valid HTTP URL hosts. DNS resolution validation does not close the surface because IP addresses (like 169.254.169.254) pass DNS resolution checks (or are treated as pre-resolved), and because DNS rebinding attacks can cause a hostname that resolves to a valid public IP at validation time to resolve to a private IP at fetch time. The structural requirement is destination address allowlist validation: after DNS resolution, the resolved IP address must be checked against a prohibited range list that includes all RFC-1918 ranges, loopback, link-local, and the server's internal VPC or private network CIDR. The check must occur immediately before the fetch (not at input validation time) to prevent DNS rebinding. The HTTP client used for server-side fetches must be configured to disable automatic redirect following (to prevent SSRF via redirect chains to internal addresses), set strict timeouts (to prevent SSRF via slow-server attacks that consume connection pool slots), and use the resolved IP address directly for the connection (not re-resolve the hostname for the connection, which would be vulnerable to DNS rebinding). For applications that handle untrusted URLs at scale, an additional isolation boundary — performing all server-side fetches from a network-isolated container with no outbound access to the private network — closes the SSRF surface architecturally rather than through validation logic alone. The security scanning decision record documents the SSRF testing requirement in the security testing pipeline — URL-accepting endpoints must be tested with SSRF payloads (private IP ranges, instance metadata URLs, redirect chains to private IPs) as part of the security scan that runs on each deployment. The zero-trust network access decision record documents the network segmentation policy — the VPC design that isolates the application server's outbound access to permitted external ranges, which reduces the impact of SSRF vulnerabilities that are not caught by validation logic by preventing the server-side fetch from reaching internal resources even if the destination validation is bypassed.
What the founding session records and what it omits
The founding API security session — or more typically, the series of sessions that establish the API framework, the authentication middleware, and the first set of input validators — records the authentication mechanism chosen (JWT, session cookie, API key), the location where it is enforced (gateway middleware, application middleware, or both), and the rate limiting configuration applied. What it does not record is the defense-in-depth model: the explicit statement that gateway authentication is a minimum enforcement point and that application-layer authentication is a required additional enforcement layer, present at every handler. Without the explicit statement, the defense-in-depth requirement is absent from the architectural record, and the natural consequence is that developers who add endpoints understand "we use JWT auth at the gateway" as "authentication is handled by the gateway" — a reasonable reading of the documentation, but an incomplete one that treats the gateway as the exclusive enforcement point rather than the first of two.
The founding session also does not record the differentiated security model for authentication endpoints versus application endpoints. Most API endpoints are designed with the assumption that authenticated callers have passed the API's baseline trust verification, and the security question is "what can this authenticated caller do?" The login endpoint is designed for unauthenticated callers whose trust level is unknown, and the security question is "how do we prevent automated attacks from validating credentials at scale?" The two questions require different policy responses — the application endpoint question is answered by authorization logic (has this authenticated caller been granted access to this resource?), and the login endpoint question is answered by authentication abuse prevention (how many failed attempts from any source is this account permitted before we require additional verification?). Treating the login endpoint identically to other endpoints under a uniform capacity rate limiting policy is a category error: the login endpoint's adversarial model is an attacker with a credential list, not a legitimate client with excessive request volume, and per-IP rate limiting addresses the latter but not the former.
The founding session also does not record the SSRF threat model for URL-accepting inputs, because the SSRF vulnerability class requires knowing that the server performing the fetch has access to resources that the caller does not — and at the time a URL input field is first added to an API, the internal network topology (what the server can reach that a public caller cannot) may not be salient to the developer adding the field. The developer validates the URL format because malformed URLs would cause the HTTP client to fail; they validate DNS resolution because unresolvable hostnames would cause the fetch to fail. Neither validation is designed with an adversarial model in mind — they are correctness validations, not security validations. The SSRF validation (destination address allowlist) is a security validation that requires an explicit understanding that the server's network access is different from the caller's network access, and that this difference creates a privilege escalation path that a caller can exploit by supplying a URL pointing to a resource the caller cannot directly access. Recording this threat model in the founding session — or in the API endpoint implementation guidelines — converts it from an implicit requirement (known only to engineers with SSRF experience) to an explicit requirement (applied by any developer who reads the guidelines when adding a URL-accepting input).
The accumulated security impact of these three omissions arrives through three different mechanisms and at three different timescales. The authentication enforcement boundary omission produces a bypass vulnerability that is introduced by a future developer acting in good faith — there is no attacker involved in the introduction; the bypass exists because a legitimate operational decision (lower latency for webhook receivers) was made without the context that the application server has no independent authentication layer. The vulnerability waits passively until a security researcher (in the best case) or an attacker (in the worst case) discovers the pattern. The rate limiting policy omission produces a vulnerability that requires an external trigger — a public credential breach database that happens to include users whose email addresses are guessable from the company's public presence. The vulnerability is latent at launch (no breach database contains the company's users' passwords yet) and becomes exploitable as the company's users accumulate account history across multiple services. The SSRF omission produces a vulnerability that is stable until the infrastructure changes — the risk is low when the application server has minimal internal network access and high when the IAM role is expanded to access sensitive resources (as naturally happens when product features require S3 access, SQS access, RDS access, and so on). Each expansion of the IAM role or internal network access expands the SSRF impact without any change to the URL validation logic. The CI/CD pipeline security decision record documents the security testing gate in the deployment pipeline — the SAST checks that flag caller-supplied URL inputs without destination validation, the API security tests that verify the login endpoint's account-level lockout behavior, and the endpoint inventory check that flags application routes not covered by the authentication middleware. The access control model decision record documents the horizontal authorization model — the policy that validates caller-supplied identity parameters against the authenticated session's authorized scope, which is the application-layer check that prevents the tenant ID enumeration pattern independent of the gateway's JWT validation. The WhyChose decision extractor finds the API security founding sessions in your AI chat exports — the "how should we handle authentication?" architecture discussion, the "what rate limits should we set?" infrastructure planning session, the "let's add webhook delivery" feature design conversation. It extracts the authentication boundary decision, the rate limit configuration, and the URL validation approach from those sessions and surfaces the defense-in-depth requirement, the login-specific lockout model, and the SSRF destination policy that the sessions documented versus the ones they omitted — the decisions that determine whether a webhook endpoint added for latency reasons becomes a tenant data enumeration path, whether a per-IP rate limit stops a 1,840-IP credential stuffing campaign, and whether a URL validator that checks syntax lets a caller retrieve EC2 IAM credentials on demand.
The five ADR sections for an API security decision
Section 1: Authentication enforcement boundary requirements and defense-in-depth. Specify the authentication enforcement model as a defense-in-depth policy, not a single enforcement point policy. The policy has two required enforcement layers: gateway authentication (JWT validation, API key verification, or equivalent at the ingress layer that handles all publicly routable traffic) and application-layer authentication (middleware in the application framework that validates the authentication token on every request handler). Both layers are required; neither is sufficient alone. The gateway layer's failure mode is bypass — an endpoint added outside the gateway routing path, a gateway misconfiguration, or a gateway software bug can cause unauthenticated requests to reach the application. The application layer's failure mode is omission — a developer who does not attach the authentication middleware to a new handler creates an unprotected endpoint. Defense-in-depth means that the bypass of one layer does not create an unprotected endpoint: the application layer checks authentication even on requests that arrived via a non-gateway path, and the gateway checks authentication even on requests that the application layer would also check. Specify the opt-out model for public endpoints: authentication middleware is applied to all request handlers by default, and endpoints that are intentionally public (health check endpoints, public documentation endpoints, public webhook receiver endpoints where authentication is handled by payload signature verification) must be explicitly marked as public-exempt in the route configuration. The explicit opt-out produces a code artifact that documents the security decision: every route in the application either carries the authentication middleware or carries a public-exempt marker with a comment explaining why the route is exempt and what alternative security control (if any) protects it. Specify the caller-supplied identity parameter policy: any request handler that accepts a caller-supplied identity parameter (tenant_id, user_id, account_id, organization_id, or any other parameter that determines whose data is queried) must validate that the authenticated session's authorized scope includes the requested identity before using the parameter. The validation is an explicit authorization check — not a database lookup that happens to fail for unauthorized IDs (because database lookup errors and authorization errors have different semantics), but an explicit comparison between the session's authorized tenant or user scope and the requested parameter value, returning a 403 Forbidden response if the comparison fails. Specify the endpoint inventory requirement: every API endpoint must be registered in a machine-readable endpoint inventory (a YAML or JSON file committed to the repository) that records the endpoint path, the HTTP method, the authentication enforcement points (gateway and/or application middleware), the authorization checks performed (none for public endpoints, session-scope validation for tenant-scoped endpoints, role check for privileged endpoints), and any caller-supplied identity parameters and their validation requirements. The inventory is checked in CI: a test compares the inventory against the application's route table and fails if any route is present in the application but not in the inventory, or if any route's inventory entry shows fewer enforcement points than the policy requires. The authentication strategy decision record documents the JWT claim structure, the token lifetime policy, and the token refresh model — the technical specification for the authentication tokens that the gateway and application middleware validate. The authorization model decision record documents the authorization policy for cross-entity access — the conditions under which an authenticated caller is permitted to access another entity's resources, and the enforcement mechanism for the caller-supplied identity parameter validation.
Section 2: Login endpoint rate limiting and credential stuffing prevention policy. Specify the authentication endpoint rate limiting policy separately from the API's general capacity rate limiting policy, because the two policies address different threat models with different control mechanisms. The capacity rate limiting policy (documented in the API rate limiting decision record) is designed to prevent a single caller from consuming a disproportionate share of server resources; its mechanism is per-IP and per-authenticated-user request count thresholds. The credential stuffing prevention policy is designed to prevent automated tools from testing large numbers of credential pairs against the authentication endpoint; its mechanism is account-level failed-attempt counting that is independent of source IP. Specify the account-level lockout policy: the authentication endpoint increments a failed-attempt counter for each email address on every failed authentication attempt, regardless of the source IP address of the request. When the counter for an email address reaches the lockout threshold (a recommended starting point is 5 failed attempts within any 15-minute window), the authentication endpoint returns a 429 response for all subsequent authentication attempts for that email address until the account is unlocked via email verification or password reset. The counter resets on successful authentication. The lockout threshold is a trade-off between security (lower threshold reduces the number of attempts the attacker can make) and usability (lower threshold increases the likelihood that a legitimate user who misremembers their password is locked out); the 5-attempt / 15-minute window accommodates a user who tries a few password variations while being significantly below the attempt volume required for practical credential stuffing (which requires thousands of attempts per target account to have a meaningful success rate against bcrypt-hashed passwords). Specify the CAPTCHA challenge policy: after the second failed authentication attempt for an email address within a session or IP context, the authentication endpoint requires a CAPTCHA challenge before processing the next attempt. The CAPTCHA challenge is triggered on the second attempt (not the first) to avoid challenging users who mistype their password once; it is triggered before the third attempt (not after lockout) to reduce the number of attempts the attacker can make before human-solvable interruption. The CAPTCHA implementation must meet two requirements: it must not be bypassable by the credential stuffing tool (which means it must not use a CAPTCHA service whose API the tool can call directly), and it must not create an accessibility barrier for users with visual impairments (which means it must offer an audio alternative or a non-visual challenge). Specify the bot scoring integration: the CDN or load balancer layer must emit a bot-probability score for each request based on signals including User-Agent string (static and outdated User-Agents are correlated with automation), request header completeness (missing standard browser headers such as Accept-Language, Accept-Encoding, and DNT are correlated with non-browser HTTP clients), request timing patterns (sub-human form submission times, uniform inter-request intervals), and ASN classification (known datacenter ASNs, VPN providers, and proxy services are correlated with automation). The bot score must be forwarded to the authentication endpoint as a request header, and the authentication endpoint must apply a response policy based on the score: requests scoring above the high-confidence bot threshold are rejected with a 429 before authentication processing begins; requests scoring in the medium-confidence range trigger the CAPTCHA challenge; requests scoring below the low-confidence threshold are processed normally. The bot scoring integration does not replace the account-level lockout policy — both are required, because bot scoring can be evaded (sophisticated credential stuffing tools use browser automation that produces realistic bot scores), and the account-level lockout is the backstop that limits the attacker's ability to attempt credentials even when bot scoring fails to detect the attack. The API rate limiting decision record documents the capacity-oriented rate limiting configuration that applies to the authentication endpoint as part of the global API rate limit, in addition to the credential-stuffing-specific controls documented in the API security decision record. The API rate limiting and API security records together define the complete rate limiting policy for the authentication endpoint: the capacity limit (from the rate limiting record) and the credential stuffing prevention controls (from the security record).
Section 3: Input validation surface and SSRF prevention policy. Specify the input validation policy for URL-type inputs that trigger server-side network requests, as a distinct validation category from URL-type inputs that are stored and returned to the caller without triggering a server-side fetch. The SSRF policy applies to the former; standard URL format validation applies to both. The SSRF policy has four requirements. Requirement 1: DNS resolution with IP validation. When a URL-type input is submitted that will trigger a server-side fetch, resolve the URL's hostname to its IP address using the application server's DNS resolver immediately before the fetch (not at input validation time). Validate the resolved IP address against the prohibited destination list: RFC-1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), and the VPC's internal CIDR range. Reject the URL with a 400 error if the resolved IP falls in any prohibited range. Requirement 2: DNS rebinding prevention. Perform the DNS resolution once, validate the resolved IP, and pass the resolved IP address directly to the HTTP client for the TCP connection, with the Host header set to the original hostname. Do not allow the HTTP client to re-resolve the hostname — this prevents the DNS rebinding attack pattern where a hostname resolves to a valid public IP at validation time and switches to a private IP for the actual connection. Most HTTP client libraries provide an option to supply the resolved IP address directly; for clients that do not, an alternative is to use a custom DNS resolver that caches results for the connection lifetime and reuses the validated resolution. Requirement 3: HTTP client security configuration. The HTTP client used for server-side URL fetches must be configured with: redirect following disabled (redirect chains can lead through a valid public URL to a private address; the application must not follow redirects for server-side fetches, instead returning the initial response to the caller if the initial response is a redirect); a strict connection timeout (2-5 seconds) and a read timeout (10-30 seconds depending on the expected response size) to prevent SSRF via slow-server attacks that consume connection pool capacity; a maximum response size limit to prevent SSRF via responses that attempt to exhaust memory; and response Content-Type validation (if the server-side fetch is intended to receive a specific content type such as JSON or HTML, reject responses with unexpected content types). Requirement 4: Network isolation (for applications with high-value internal network access). For applications where the application server's network access includes high-value internal resources (databases, internal APIs, cloud provider metadata, Kubernetes cluster services), implement server-side URL fetches in an isolated network container that has outbound access only to the public internet (via an allowlisted egress firewall rule) and no access to private network ranges. The isolated container receives the fetch request from the main application, performs the fetch, and returns the response — if the SSRF destination validation fails or is bypassed, the isolated container's network access restriction prevents the fetch from reaching internal resources. This architectural control provides defense-in-depth for the SSRF policy: even a successful bypass of the IP validation logic cannot reach internal resources from the isolated container. Specify the testing requirement for URL-accepting endpoints: every endpoint that accepts a URL-type input and performs a server-side fetch must be tested with a suite of SSRF payloads as part of the endpoint's integration tests. The payload suite must include: a URL with a private IP address (10.0.0.1), a URL with the loopback address (127.0.0.1), a URL with the link-local metadata address (169.254.169.254), a URL with an IPv6 private address (::1), and a URL with a hostname that resolves to a private IP address (requires a test DNS record that maps a test hostname to a private IP). Each test must verify that the endpoint returns a 400 error (not a 500 or 200) for the SSRF payload. The security scanning decision record documents the DAST (dynamic application security testing) integration that runs SSRF probes against all API endpoints as part of the security scan, supplementing the endpoint-specific integration tests with a broader scan that catches new URL-accepting inputs that may have been added without SSRF tests. The zero-trust network access decision record documents the VPC network segmentation that limits the application server's internal network access to the minimum required for its function, which determines the SSRF blast radius — the smaller the internal network access, the smaller the set of resources an SSRF-exploiting caller can access.
Section 4: API key and token management requirements. Specify the lifecycle management requirements for the authentication artifacts that the API security policy relies on — JWT tokens, API keys, session tokens, and webhook signing secrets. The specification has five components. Token lifetime policy: JWT access tokens must have a short lifetime (15-60 minutes) so that a compromised token has a bounded validity window; refresh tokens must have a longer lifetime (7-30 days) and must be stored securely (HttpOnly cookies, not localStorage) to resist XSS exfiltration; API keys for machine-to-machine integrations must have configurable expiration and must support rotation without service interruption (the application must accept the old and new key during a rotation window and revoke the old key after the window expires). Token revocation policy: the authentication service must maintain a token revocation list that allows immediate revocation of specific tokens or all tokens for a specific user (on account compromise or password change); the revocation list must be checked on every authenticated request with a latency budget that does not materially degrade the request latency (use a local cache with a short TTL rather than a database lookup per request); revoked tokens must be rejected with a 401 response even if they are within their validity window. API key scoping policy: API keys must be issued with an explicit scope declaration (the set of endpoints and operations the key is authorized to use) and the application must enforce the scope on every request that presents an API key; a key issued for read-only access to the events API must be rejected with a 403 if it is used to call a write endpoint, even if the key is otherwise valid. Webhook signing secret lifecycle: webhook signing secrets must be rotatable without requiring the webhook sender to update their integration simultaneously (the application must accept signatures computed with either the current or the previous secret during a rotation overlap window of 24-48 hours); the rotation event must be logged (timestamp, which account, which secret generation); the secret must be stored as a hashed value in the database and only returned to the user once at creation (not subsequently retrievable). Credential storage requirements: all authentication secrets (API keys, webhook secrets, session tokens) must be stored as hashed values in the database; the application must compare against the hash rather than storing or comparing against the plaintext value; hash functions must be bcrypt or Argon2 for secrets that are vulnerable to offline brute-force attacks, and HMAC-SHA256 for secrets with sufficient entropy (256-bit random API keys) where a one-way hash is sufficient. Specify the secret rotation schedule: all long-lived secrets (API keys, webhook secrets, server-to-server credentials) must be rotated at least annually, with automated reminders at 60 days before expiration; the rotation process for each secret type must be documented in the operational runbook with step-by-step instructions that do not require engineering team involvement for a standard rotation. The secrets management decision record documents the secrets storage infrastructure — the vault or key management service used to store application secrets, the access policy for reading secrets at runtime, and the audit log for secret access. The secrets rotation decision record documents the rotation procedure, the consumer update window, and the compromised credential response time — the playbook for rotating a secret when a credential is suspected to be compromised, including the time targets for rotation, consumer notification, and revocation.
Section 5: Security testing integration and API security monitoring requirements. Specify the security testing gates that verify the API security policy is correctly implemented across all endpoints as the API grows. The security testing requirements have four layers. Layer 1: Authentication coverage test. A CI test that inspects the application's route table at test time (not at review time) and verifies that every route either has the authentication middleware attached or appears in the explicit public-exempt registry. The test fails if a new route is added without the middleware and without a public-exempt entry — it catches authentication omissions at PR merge time, before they reach production. The test does not require sending real HTTP requests; it inspects the framework's route registration data structure directly, which makes it fast (milliseconds) and independent of a running server. Layer 2: Endpoint inventory synchronization test. A CI test that compares the machine-readable endpoint inventory against the application's route table and fails if any route is not in the inventory or if any route's inventory entry is stale (the authentication enforcement points in the inventory do not match what is actually registered in the route table). This test enforces inventory maintenance as a condition of CI passage rather than as a code review expectation. Layer 3: SSRF payload integration tests. Per the input validation policy, every URL-accepting endpoint must have integration tests that verify SSRF payloads return 400 errors. These tests run in the standard integration test suite on every PR. A test that covers the private IP payloads, loopback, link-local, and IPv6 private addresses must be present in the test file for each URL-accepting endpoint; a CI check verifies that URL-accepting endpoints (identified by their route or handler type annotation) have a corresponding SSRF test file. Layer 4: Security scanning pipeline. A DAST security scan runs on every deployment to the staging environment (not only on a periodic schedule) and sends SSRF probes, credential stuffing simulation probes (verifying that the account lockout policy triggers within the specified attempt threshold), and authorization bypass probes (testing caller-supplied identity parameters with unauthorized IDs) against all API endpoints. The scan results are required for deployment to production — a failed scan blocks the deployment with a report of the specific probes that failed. Specify the security monitoring requirements: the authentication service must emit the following metrics and alerts: (1) failed authentication rate per email address (alert when any single email address exceeds 5 failed attempts in 15 minutes — the lockout threshold); (2) total failed authentication rate across all email addresses (alert when the rate exceeds 3× the trailing 7-day average — an early signal for distributed credential stuffing before individual account lockouts trigger); (3) authentication from new geolocations (alert when an account authenticates from a country not observed in the account's prior 90-day login history — a signal for account compromise); (4) SSRF probe attempts in the access log (alert on any request to a URL-accepting endpoint where the submitted URL resolves to a private IP range — the alert fires even if the SSRF validation correctly rejects the request, because a probe attempt indicates an active attacker testing the endpoint). The monitoring metrics serve a distinct purpose from the testing gates: testing verifies the security controls are implemented; monitoring detects when those controls are being tested or circumvented by an attacker and provides the signal for incident response. The observability strategy decision record documents the metrics infrastructure and alerting pipeline — the tools used to collect, aggregate, and alert on the security metrics specified in the API security decision record. The audit log decision record documents the audit trail requirements for security-relevant events — the authentication decisions (successful and failed), the account lockout events, the token revocation events, and the security scan results — that are required for forensic investigation when a security incident is discovered, providing the evidence needed to determine the scope of a breach and the accounts affected.