Chapter 21
API Security
Scope. This chapter owns API Gateway, Cloud Endpoints, and Apigee for the whole book, together with the authentication, authorization, and metering that sit in front of an API: OAuth and OIDC, JWT validation, API keys, service identities, rate limits and quotas, logging, and the internal, external, and multi-tenant architectures. §10.27 already defers here for per-consumer authentication and API keys. Edge filtering is Chapter 19; the data perimeter is Chapter 20. Prerequisites. Chapter 4 (§4.7 impersonation, §4.13 Workload Identity Federation, §4.16 Identity-Aware Proxy), Chapter 10 (Cloud Run), Chapter 19 (§19.6 rate limiting). Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0,
hashicorp/googleprovider 8.x; see sources at end.
An API is where an organization's authorization model becomes an external contract. Everything before this chapter has been about controlling access to Google Cloud resources by Google Cloud principals; an API inverts the problem — the caller is someone else's software, holding a credential you issued, and the boundary you enforce is one you wrote rather than one Google enforces for you.
Google Cloud offers three products for that boundary and they are not interchangeable. API Gateway is a managed proxy in front of serverless backends, configured by an OpenAPI document, and it is the right default for a service that needs authentication, per-consumer keys, and quota with no operational surface. Cloud Endpoints is the older sibling — the same ESP-derived proxy, but you run it, usually as a sidecar, which is what you want when the proxy must live inside your own network. Apigee is a full API management platform with a policy engine, developer portal, monetization, and its own runtime, and it is a product you adopt rather than a feature you enable.
The failure mode this chapter is written against is not choosing wrongly among the three. It is deploying one and believing the security problem is solved. A gateway validates tokens and counts requests; it does not know which tenant's data a request should reach. Object-level authorization — this consumer may read this record — is application logic, always, and §21.5 says so explicitly because the number of breaches caused by getting it wrong exceeds the number caused by everything else in this chapter combined.
21.1 API Gateway §
API Gateway is a fully managed proxy that fronts serverless backends — Cloud Run, Cloud Run functions, App Engine — and enforces authentication, key checking, and quota from an OpenAPI 2.0 document.
Three resources, deployed in order:
| Resource | What it holds | Command |
|---|---|---|
| API | The logical API, a container | gcloud api-gateway apis create |
| API config | One immutable version of the OpenAPI spec plus its backend identity | gcloud api-gateway api-configs create |
| Gateway | A regional deployment serving one API config | gcloud api-gateway gateways create |
gcloud api-gateway apis create billing-api --project=rc-saas-prod-app-01
gcloud api-gateway api-configs create billing-v1 \
--api=billing-api \
--project=rc-saas-prod-app-01 \
--openapi-spec=openapi/billing-v1.yaml \
--backend-auth-service-account=sa-gw-billing@rc-saas-prod-app-01.iam.gserviceaccount.com
gcloud api-gateway gateways create billing-prod \
--api=billing-api \
--api-config=billing-v1 \
--location=us-central1 \
--project=rc-saas-prod-app-01
API configs are immutable, and that is the deployment model. A change to the spec is a new config, and the gateway is updated to point at it. Rollback is repointing at the previous config, which makes API configuration changes as revertible as a Cloud Run revision (§25.9).
--backend-auth-service-account is the identity the gateway presents to your backend. It is what lets the backend require authentication and refuse everything that did not come through the gateway — the API equivalent of origin protection (§19.11). Give it exactly roles/run.invoker on the target service and nothing else.
Pitfall. A gateway does not protect a backend that is also reachable directly. A Cloud Run service behind API Gateway must have roles/run.invoker granted only to the gateway's service account and ingress restricted; otherwise the gateway is an optional path and every control in this chapter is optional with it.
21.2 Cloud Endpoints §
Cloud Endpoints is the self-hosted form of the same proxy technology. You deploy a service configuration to Google's Service Management, and run the Extensible Service Proxy alongside your application.
The trade is control for operations. The proxy runs in your network — as a GKE sidecar, a container in the same Cloud Run service, or a process on a VM — which means it can reach backends that a managed gateway cannot and can be placed inside a service perimeter (Chapter 20). In exchange, you own its version, its resource footprint, and its upgrades.
gcloud endpoints services deploy openapi/billing-v1.yaml \
--project=rc-saas-prod-app-01 --validate-only
gcloud endpoints services deploy openapi/billing-v1.yaml \
--project=rc-saas-prod-app-01
--validate-only belongs in the pipeline, before the real deploy. Service configuration errors are rejected at deploy time, and a rejected deploy in a delivery pipeline is a failed release rather than a broken API.
-f / --force overrides what Google classifies as hazardous changes — removing a method, tightening a quota, changing an authentication requirement. That flag is a signal that consumers will break, so it belongs behind a change process, never in an automated apply.
Choose Endpoints over API Gateway for three reasons only: the backend is not serverless, the proxy must be inside your VPC or perimeter, or you need a proxy version you control. Otherwise the managed gateway removes work you do not need to own.
Pitfall. The service configuration is deployed to a global Service Management surface keyed by service name, but the proxy is deployed by you. The two drift: a config deployed months ago is still authoritative for a proxy nobody has restarted, and the symptom is an API enforcing rules nobody can find in the current repository.
21.3 Apigee §
Apigee is a full API management platform: a runtime with a policy engine, a developer portal, analytics, and monetization. It is a different scale of commitment from the other two.
Its resource model is four objects deep:
| Object | What it is |
|---|---|
| Organization | The top-level Apigee tenant, bound to one Google Cloud project |
| Environment | A deployment target — prod, test — holding deployed API proxies |
| Environment group | A hostname routing layer mapping domains to environments |
| Instance | The regional runtime that actually serves traffic |
Provisioning is a choice between Subscription and Pay-as-you-go, and the organization's runtime_type is CLOUD or HYBRID — the latter running the runtime plane in your own Kubernetes while the management plane stays with Google.
Encrypt the runtime disk with your own key. The Apigee instance resource takes disk_encryption_key_name, documented as a "Customer Managed Encryption Key for disk/volume encryption (required for paid subscriptions)" — wire it to a key in rc-saas-shared-sec-01 following the CMEK pattern in §14.8.
Advanced API Security is the security-relevant add-on, comprising detection rules, security reports, risk assessment, and abuse detection — behavioral analysis over API traffic that the other two products do not attempt.
Adopt Apigee when the API is the product. A public developer platform with third-party consumers, tiered plans, and a portal is what Apigee is for. An internal service with four callers behind a gateway is not, and running Apigee for it is an operational cost with no matching benefit.
Pitfall. Apigee's policy engine can implement authentication, authorization, transformation, and threat protection, which makes it tempting to move business logic into proxy policies. That logic then lives outside your source repository's test suite and outside the delivery pipeline of Chapters 22 through 25. Keep proxies thin.
21.4 Authentication §
Authentication answers who is calling. In Google Cloud's API products it is always a signed token validated against a published key set — never an API key, which is §21.9's subject and identifies a project rather than a caller.
Three caller populations, three mechanisms:
| Caller | Credential | Validated against |
|---|---|---|
| Another Google Cloud workload | A Google-signed ID token with your API as the audience | Google's public key set |
| A partner's own service | A JWT signed by their service account or IdP | Their published JWKS URI |
| An end user | An OIDC ID token from your identity provider | The provider's JWKS URI |
The configuration is a securityDefinitions block in the OpenAPI document, and three extensions carry it: x-google-issuer, x-google-jwks_uri, and x-google-audiences.
securityDefinitions:
partner_sa:
authorizationUrl: ""
flow: implicit
type: oauth2
x-google-issuer: "sa-partner@PARTNER_PROJECT.iam.gserviceaccount.com"
x-google-jwks_uri: "https://www.googleapis.com/robot/v1/metadata/x509/\
sa-partner@PARTNER_PROJECT.iam.gserviceaccount.com"
x-google-audiences: "https://billing.rickcollette.domain"
For service-to-service calls, Google describes the flow precisely: "The calling service uses the service account's private key to sign a secure JSON Web Token (JWT) and sends the signed JWT in the request to your API", and "API Gateway validates that the claims in the JWT match the configuration in your API config before forwarding the request to your API."
Every definition must set an audience. A token without an audience check is replayable against any API that trusts the same issuer, which for a Google service account means every API in the world that trusts Google. The audience is what makes a token specific to your service.
Pitfall. A securityDefinitions block that is declared but not referenced by a security requirement on an operation authenticates nothing. The spec is valid, the deploy succeeds, and the endpoint is open. Assert on this in a contract test rather than in review.
21.5 Authorization §
Authorization answers what the authenticated caller may do, and it splits into two layers that must not be confused.
The gateway does coarse authorization. It can require a valid token from a named issuer, restrict an API key to particular methods, and enforce a quota per consumer. That is method-level access: may this caller invoke this operation at all?
The application does object-level authorization. May this caller read this record? The gateway has no idea, because the answer depends on data it does not have. Every broken-object-level-authorization vulnerability — the largest category in the OWASP API Security list — lives on the wrong side of this line.
Carry the identity into the backend rather than re-deriving it. API Gateway "will send the authentication result in the X-Apigateway-Api-Userinfo to the backend API. It is recommended to use this header instead of the original Authorization header. This header is base64url encoded and contains the JWT payload."
The rule that follows: the backend must reject any request that arrives without that header, and must not accept a tenant identifier from the request body or path. The tenant comes from the validated token, always; a tenant_id query parameter is an authorization bypass with a friendly name.
Judgment. Write one authorization function, called at the top of every handler, taking the caller's claims and the resource being touched. Scattering if tenant == ... checks through handlers guarantees that one handler is missing it, and that handler is the vulnerability.
Pitfall. Because the gateway strips and replaces the Authorization header, backends written to trust X-Apigateway-Api-Userinfo will trust it from any source if they are ever reachable directly. That header is only trustworthy because the gateway is the only path (§21.1).
21.6 OAuth §
OAuth 2.0 is the delegation framework: it lets a user authorize an application to call an API on their behalf, without giving the application their credentials.
Two flows matter for the APIs in this book:
- Client credentials — machine-to-machine, no user involved. The caller presents its own credential and receives an access token. In Google Cloud this is what a service account does, and §21.10 covers it.
- Authorization code with PKCE — a user grants an application access. The application never sees the user's password, and PKCE closes the interception attack on the code exchange for public clients.
The implicit flow is obsolete. It returns tokens in a URL fragment, where they land in browser history, referrer headers, and logs. Any design still specifying it is copying an old tutorial.
Google Cloud does not provide a general-purpose OAuth authorization server for your API's consumers. Identity Platform issues tokens for your application's end users, Apigee's OAuthV2 policy can act as an authorization server for API consumers, and Google's own OAuth service issues tokens for Google APIs and for service accounts. Choose one deliberately; the failure mode is a hand-rolled token service that is missing revocation.
Scopes are coarse and belong to the token; permissions are fine and belong to your model. A scope of billing.read on a token means the application may attempt read operations. Whether this user may read this invoice is §21.5's question and is not answered by the token.
Pitfall. Refresh tokens are long-lived credentials with all the properties of a service account key. They need storage in Secret Manager (§13.7), revocation on user offboarding, and rotation — and if your design has no revocation path, the access it grants is effectively permanent.
21.7 OIDC §
OpenID Connect is the identity layer over OAuth 2.0: it adds an ID token, a signed JWT asserting who the user is, alongside the access token that says what the client may do.
The two tokens are not interchangeable and swapping them is a real vulnerability. An ID token is an assertion about a user, minted for a specific client; an access token is a bearer credential for an API. Accepting an ID token as an API credential means accepting a token whose audience is a client application, which any application sharing that IdP can obtain.
Discovery is what makes validation maintainable. An OIDC provider publishes /.well-known/openid-configuration naming its issuer and its JWKS URI, and the gateway fetches signing keys from there — so key rotation at the provider requires no change on your side. That is exactly what x-google-issuer and x-google-jwks_uri (§21.4) configure.
Claims to validate, in order of how often they are skipped:
| Claim | Check |
|---|---|
iss | Exactly matches the configured issuer, string comparison |
aud | Contains your API's audience |
exp | Not expired, with a small clock-skew tolerance only |
nbf, iat | Not used before valid, not implausibly old |
sub | The stable user identifier — use this, not email |
Never key user records on email. It is mutable at most providers and is not guaranteed unique across time. sub is the identifier the provider promises is stable; joining on email is how one user inherits another's account.
Pitfall. OIDC federation for workloads into Google Cloud is Workload Identity Federation (§4.13) and is a different problem: there you are the relying party for a CI system's token. Both use OIDC and the two configurations are routinely confused in a repository, with the API's issuer pasted into a workload identity pool.
21.8 JWT Validation §
A JWT is a signed set of claims. Validating one correctly is a short list, and every item on it has been the root cause of a public breach.
The mandatory sequence:
- Parse without trusting. Read the header for the algorithm and key ID; do not act on anything in the payload yet.
- Reject
alg: noneand reject algorithm confusion. The verifier must be configured with the expected algorithm, not take it from the token. An RS256 verifier that accepts HS256 lets an attacker sign with the public key. - Fetch the key by
kidfrom the configured JWKS URI, with caching and a bounded refresh. Never from a URL in the token. - Verify the signature.
- Then validate the claims in §21.7's table.
Let the gateway do it. API Gateway and Cloud Endpoints implement this correctly, which is most of their value. A backend behind them validates nothing and trusts the forwarded header (§21.5); a backend without them must implement the full sequence with a maintained library, never by hand.
Custom JWT locations are supported and are occasionally necessary. x-google-jwt-locations lets a token arrive in a named header or query parameter with an optional value_prefix, for consumers who cannot send a standard Authorization header.
Tokens in query strings end up in logs. Load balancer request logs, proxy logs, and browser history all record the full URL. If x-google-jwt-locations puts a token in a query parameter, that token is now in Cloud Logging (§17.6) with a 30-day retention and whatever IAM the log view grants.
Pitfall. A JWT is signed, not encrypted. Its payload is base64url and readable by anyone who intercepts it — including the consumer, the browser, and every proxy in between. Never put anything in a claim you would not put in a URL.
21.9 API Keys §
An API key identifies the project an API call is associated with. It is not authentication, it does not identify a caller, and it should never be the only thing between a request and your data.
What it is legitimately for: attributing usage to a consumer for quota (§21.12), enabling a service for a project, and blocking a specific consumer quickly. What it is not for: proving who is calling.
Restrictions are what make a key survivable. gcloud services api-keys create takes mutually exclusive client restrictions plus a repeatable API restriction:
| Flag | Restricts to |
|---|---|
--allowed-ips | "the caller IP addresses that are allowed to make API calls with this key" |
--allowed-referrers | "regular expressions for the referrer URLs that are allowed" |
--allowed-bundle-ids | "iOS app's bundle ids that are allowed to use the key" |
--allowed-application | Android apps, by sha1_fingerprint and package_name |
--api-target | Specific services, and optionally specific methods |
gcloud services api-keys create \
--project=rc-saas-prod-app-01 \
--display-name="Partner integration — Contoso" \
--allowed-ips=203.0.113.0/24 \
--api-target=service=billing-api.endpoints.rc-saas-prod-app-01.cloud.goog
Always set --api-target. An unrestricted key works against every API the project has enabled, so a key leaked from a mobile app becomes a credential for services the app never called.
A service-account-bound key is a different object. --service-account makes the key, in Google's words, "a service account bound key and auth enabled" — which turns it into an authentication credential and therefore into a long-lived secret with all the handling requirements of §4.9. Prefer a signed token.
Pitfall. Keys embedded in a mobile app or a single-page application are public by construction — decompilation and the browser's network tab both reveal them. Referrer and bundle restrictions raise the cost of misuse and do not prevent it, so a public-client key must be paired with real user authentication (§21.7) and a low quota.
21.10 Service Identities §
A service identity is the credential one workload uses to call another. In Google Cloud the correct form is a short-lived, audience-scoped ID token minted for an attached service account.
The pattern, end to end:
- The calling workload runs as a service account attached to its runtime — no key file (§4.9).
- It requests an ID token from the metadata server, with the callee's URL as the audience.
- It sends the token as
Authorization: Bearer <token>. - The callee — API Gateway, or Cloud Run's own IAM check — validates the signature and the audience.
Naming, per this estate: the gateway's backend identity is sa-gw-<api>, the caller is sa-run-<service> or sa-app-<env> (§8.x, §10.x). One service account per service, never one shared across an environment, because the whole point is that the audit log names the caller.
For callers outside Google Cloud, federate. A partner's CI system, an on-premises service, or a workload in another cloud uses Workload Identity Federation (§4.13) to exchange its own OIDC token for a short-lived Google credential. This is the alternative to issuing a service account key to a third party, which this book never recommends.
Impersonation chains must be short and logged. Where a service must act as another identity, iam.serviceAccounts.getAccessToken on the target is the grant, and §4.7 owns the model. Two hops is a design smell; three is an audit trail nobody can follow (§17.19).
Pitfall. An ID token minted for audience A is valid at any endpoint that checks for audience A. Set the audience to the exact service URL, not to a shared domain, or one compromised service's token opens every service behind the same name.
21.11 Rate Limiting §
Rate limiting for an API means limiting per consumer, using an identity the API knows — which is what distinguishes it from the edge rate limiting of §19.6.
Two layers, and both are needed:
| Layer | Keyed on | Purpose |
|---|---|---|
| Cloud Armor (§19.6) | IP, header, TLS fingerprint, ASN | Abuse and volumetric defense, before authentication |
| Gateway quota (§21.12) | The consumer's API key or project | Fair use and entitlement, after identification |
Edge limits protect the platform; consumer limits protect the business. An edge limit stops a flood from taking the service down. A consumer limit stops one customer's runaway retry loop from consuming the capacity you sold to everyone else — and produces the 429 that tells them to fix their client.
Return 429 with Retry-After. A limit that returns 500 or silently drops teaches clients to retry harder. A limit that returns 429 with a retry hint and a documented backoff expectation is a limit clients can comply with.
Set limits per plan, not per endpoint. A consumer on a free tier and one on an enterprise contract need different numbers against the same operations, which is why the quota mechanism keys on the consumer rather than on the path.
Pitfall. Rate limiting an authenticated API only at the edge, keyed by IP, penalizes exactly the consumers who deploy behind a NAT — which is every enterprise customer. Key on the consumer wherever the consumer is knowable.
21.12 Quotas §
A quota is a consumption limit attached to a consumer over a period. Two distinct systems provide them and they are frequently conflated.
Your API's quotas are declared in the OpenAPI document, through x-google-management, which "Controls API management features via metrics (defines quota metrics), quota (sets quota limits), and x-google-quota (associates methods with metrics via metricCosts)".
x-google-management:
metrics:
- name: "billing-requests"
displayName: "Billing API requests"
valueType: INT64
metricKind: DELTA
quota:
limits:
- name: "billing-requests-limit"
metric: "billing-requests"
unit: "1/min/{project}"
values:
STANDARD: 1000
metricCosts is the underused part. An expensive operation can cost ten units of the same metric as a cheap one, so a single quota expresses "this much work per minute" rather than "this many calls per minute" — which is what you actually want to sell.
Google Cloud's own service quotas are the other system. They limit your project's consumption of Google APIs, and are adjusted with a consumer quota override — override_value, service, metric, limit, and an optional dimensions map, with a force flag "to bypass 10% quota decrease safety check".
Quota exhaustion is an availability event and needs an alert. A consumer hitting their limit is normal; your project hitting a Google Cloud quota is an outage in progress. Alert on the second (§18.4) with a threshold well below the limit.
Pitfall. Quotas are enforced per consumer project, and a consumer using an API key not restricted with --api-target (§21.9) may be attributed to a different project than you expect. Key attribution and quota attribution are the same mechanism, so a sloppy key policy produces a quota policy that does not hold.
21.13 API Logging §
API request logs are the record of who called what, and they are the only evidence available when a consumer disputes usage or an incident requires reconstructing an attack.
Three log streams cover a gateway-fronted API, each with a different owner and retention:
| Stream | Contains | Where it lands |
|---|---|---|
| Load balancer / gateway request logs | Method, path, status, latency, and the Cloud Armor decision | Cloud Logging, the API's project |
| Backend application logs | The application's own view, including the authorization decision | Cloud Logging, the backend's project |
| Cloud Audit Logs | Administrative changes to the API, config, and gateway | The audit trail of Chapter 17 |
Log the consumer, never the credential. Record the API key's name or the token's sub claim, not the key string and not the token. A log line containing a bearer token is a credential with your log retention and your log IAM, and it will be read by more people than the credential store ever would be.
Route API logs through the aggregated sink (§17.9) like everything else. There is no separate API logging destination, and treating gateway logs as an application concern is how they end up with a 30-day retention while the audit trail has seven years.
Instrument the authorization decision. The most valuable API log line is the one written when object-level authorization (§21.5) denies — it is the earliest signal of a consumer probing for other tenants' data, and no platform log contains it because no platform component makes that decision.
Pitfall. Request paths carry identifiers, and identifiers are data. /v1/customers/8831/invoices in a log line is a customer relationship recorded in an operational log store. Use route templates in structured log fields where this matters, the same discipline as trace span names (§18.10).
21.14 API Threat Protection §
API-specific threats are not the web application attacks of §19.3, and the controls are different.
The four that matter most, and where each is handled:
| Threat | Control | Where |
|---|---|---|
| Broken object-level authorization | An authorization function keyed on validated claims | Your application (§21.5) |
| Credential stuffing against token endpoints | Rate limiting plus an alert on the failure rate | §19.6, §18.15 |
| Payload abuse — oversized bodies, deep nesting, injection | Schema validation plus WAF JSON parsing | Gateway, §19.2 |
| Enumeration of identifiers | Non-sequential identifiers and per-consumer quota | Application, §21.12 |
Google's first-party behavioral detection is Apigee Advanced API Security, comprising detection rules, security reports, risk assessment, and abuse detection. Nothing equivalent exists in API Gateway or Cloud Endpoints — for those, the composition above is the answer.
Validate the schema at the gateway, strictly. An OpenAPI document that describes the request body is also a filter: a gateway that rejects requests not matching the schema removes an entire class of payload attacks before your code parses anything. This only works if the spec is accurate, which is an argument for generating it from the same definition the server uses.
Use opaque, non-sequential identifiers in public APIs. Sequential integers turn one authorization bug into a complete data extraction, because the attacker does not have to guess what to ask for. This is a design decision made once and impossible to retrofit cheaply.
Pitfall. A WAF cannot see inside a JSON body unless JSON parsing is enabled on the security policy (§19.2), and API traffic is almost entirely JSON bodies. An API fronted by Cloud Armor with default parsing settings has the WAF inspecting headers and paths only.
21.15 Internal APIs §
An internal API is one whose consumers are all inside your organization. The temptation is to secure it less; the correct move is to secure it differently.
Internal does not mean unauthenticated. Every service-to-service call carries an ID token with a specific audience (§21.10), because the alternative — trusting the network — fails the moment one workload is compromised, which is the scenario the rest of this book is written for.
What internal legitimately changes:
- No API keys. Callers are Google Cloud principals with real identities; a key adds nothing.
- No public exposure. An internal Application Load Balancer, a private Cloud Run service, or a service reachable only inside the perimeter (Chapter 20).
- Simpler quota. Capacity planning rather than commercial entitlement.
- Identity-Aware Proxy for human callers (§4.16), instead of an OAuth flow you operate.
Service Directory and private uptime checks apply here (§18.5), because an internal API has no public endpoint to probe from outside.
Judgment. The strongest internal API posture in this book is: private ingress, ID token authentication with a per-service audience, one service account per caller, and object-level authorization in the application — the same last item as every external API, because internal callers can be compromised too.
Pitfall. Internal APIs accumulate consumers nobody tracks, because adding one requires no contract and no key. Six months later, nobody knows who calls the service, and it cannot be changed. Require an IAM binding per consumer — roles/run.invoker granted to a named service account — so the consumer list is the IAM policy.
21.16 External APIs §
An external API is a public contract with consumers you do not control. Everything about it is harder because you cannot fix the client.
Five things must exist before the first external consumer:
- Versioning. A path or header version, with a stated deprecation policy. Without one, the first breaking change is an incident.
- A published authentication method with an audience specific to your API (§21.4).
- Per-consumer quota and rate limits (§21.11, §21.12), because an external client will retry badly.
- A documented error contract including 401, 403, 429, and the
Retry-Afterbehavior. - A revocation path. A way to disable one consumer immediately, tested before it is needed.
Expose one endpoint, through one path. A global external Application Load Balancer with Cloud Armor (Chapter 19) in front of API Gateway in front of Cloud Run. Every additional ingress is an additional place to enforce all of this, and the enforcement will diverge.
Publish less than you know. Error responses that distinguish "no such record" from "not your record" leak the existence of other tenants' data. Return the same status for both — usually 404 — and log the distinction internally (§21.13).
Pitfall. CORS on an external API is an authorization decision made in a header. Access-Control-Allow-Origin: * combined with credentialed requests is a standing invitation for any site to call your API in your users' browsers; the x-google-endpoints allowCors setting turns this on and it is not a formatting option.
21.17 SaaS API Architecture §
A multi-tenant SaaS API is where every control in this chapter is load-bearing at once, and where the tenancy model determines the blast radius of every bug.
The tenant boundary must be enforced in more than one place, because a single enforcement point is a single bug away from a full data breach:
| Layer | Enforcement |
|---|---|
| Token | The tenant is a claim in the validated JWT, never a request parameter (§21.5) |
| Application | One authorization function, called on every handler, keyed on that claim |
| Data | A tenant column with a mandatory predicate, or per-tenant datasets or schemas (§12.x) |
| Encryption | Per-tenant CMEK where the contract requires crypto-shredding (§14.8) |
The reference architecture this book uses for rc-saas: a global external Application Load Balancer with a Cloud Armor policy (§19.1), fronting API Gateway, fronting Cloud Run services with sa-run-<service> identities, reading Cloud SQL through the Auth Proxy (§12.17), with secrets from Secret Manager (§13.7) and the data projects inside sp-prod-data (§20.11).
Tenant isolation is a spectrum with a cost curve. Shared tables with a tenant predicate is cheapest and has the highest breach blast radius; a database per tenant is expensive to operate and reduces a bug to one tenant. Pick per data class rather than for the whole product — the customer records may warrant separation while the feature flags do not.
Give every tenant a distinct consumer identity. One API key or one OAuth client per tenant, so quota, rate limits, logs, and revocation all operate at tenant granularity. A shared credential across tenants makes every one of those controls useless simultaneously.
Pitfall. Background jobs and admin tooling bypass the API and therefore bypass every control described here. The batch process that exports "all customers" runs as a service account with no tenant claim, and it is the most common source of cross-tenant leakage in a SaaS product. Route internal jobs through the same authorization function, with an explicit, logged, tenant-scoped identity.
Chapter Summary §
- API Gateway is the managed default for serverless backends; Cloud Endpoints is the self-hosted proxy; Apigee is a platform you adopt when the API is the product.
- API configs are immutable, so an API change is a new config and a rollback is repointing the gateway.
- A gateway protects nothing if the backend is reachable directly — grant the invoker role only to the gateway's service account.
- Every
securityDefinitionsblock must set an audience, and must be referenced by asecurityrequirement or it authenticates nothing. - API Gateway forwards the validated claims in the base64url-encoded
X-Apigateway-Api-Userinfoheader, and Google recommends using it instead of the originalAuthorizationheader. - The gateway does method-level authorization; object-level authorization is application logic and always will be.
- The tenant comes from the validated token, never from a path, query parameter, or body field.
- The implicit OAuth flow is obsolete; use authorization code with PKCE for user flows and client credentials for machines.
- Validate
iss,aud,exp,nbf, and key user records onsub— never onemail. - Reject
alg: none, configure the expected algorithm rather than reading it from the token, and fetch keys bykidfrom the configured JWKS URI only. - An API key identifies a project, not a caller; always set
--api-target, and treat a service-account-bound key as a long-lived secret. - Service-to-service calls use short-lived ID tokens scoped to the exact callee URL, from an attached service account, never a key file.
- Edge rate limiting protects the platform; per-consumer quota protects the business, and
metricCostslets one quota express work rather than calls. - Log the consumer's identifier, never the credential, and instrument the authorization denial — no platform log contains it.
- Apigee Advanced API Security is the only first-party behavioral API threat detection; the other products compose gateway, WAF, and application controls instead.
- Internal APIs authenticate too; what changes is keys, exposure, and quota, not whether callers prove who they are.
- In a multi-tenant SaaS API, enforce the tenant boundary at the token, the application, and the data layer, and route background jobs through the same authorization function.
Security Checklist §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
| Backend invokable only by the gateway's service account | Otherwise the gateway is an optional path | gcloud run services get-iam-policy SERVICE --region=REGION |
Every securityDefinitions referenced by a security requirement | An unreferenced definition authenticates nothing | Contract test over the OpenAPI document |
x-google-audiences set on every definition | An unaudienced token is replayable against any API trusting the issuer | Inspect the deployed API config |
API keys restricted with --api-target | An unrestricted key works against every enabled API | gcloud services api-keys list --project=PROJECT_ID |
| No service-account-bound keys issued to third parties | That is a long-lived credential; federate instead (§4.13) | Key list, serviceAccountEmail field |
| Object-level authorization in one shared function | Scattered checks guarantee one handler is missing it | Code review; a test per handler asserting cross-tenant denial |
| Per-consumer quota configured | Without it one client's retry loop consumes everyone's capacity | Deployed API config x-google-management.quota |
| JSON parsing enabled on the fronting Cloud Armor policy | API bodies are JSON and are otherwise uninspected | gcloud compute security-policies describe POLICY |
| Authorization denials logged and alerted | Earliest signal of a consumer probing other tenants | Log-based metric and alert policy (§18.15) |
| No credential appears in a log line or a query string | Logs have wider readership than the credential store | Log query for Bearer and for key-shaped strings |
| Background jobs run through the same authorization path | Untenanted admin tooling is the usual cross-tenant leak | Code review of batch entry points |
Sources §
- https://cloud.google.com/api-gateway/docs — API Gateway concepts and resource model (last validated 2026-09-03)
- https://cloud.google.com/api-gateway/docs/authenticate-service-account — service-account JWT authentication and the
X-Apigateway-Api-Userinfoheader (last validated 2026-09-03) - https://cloud.google.com/endpoints/docs/openapi/openapi-extensions — the
x-google-*OpenAPI extensions (last validated 2026-09-03) - https://cloud.google.com/endpoints/docs/openapi/authenticating-users — Cloud Endpoints authentication methods (last validated 2026-09-03)
- https://cloud.google.com/apigee/docs/api-platform/get-started/what-apigee — Apigee provisioning options and Advanced API Security components (last validated 2026-09-03)
- https://cloud.google.com/docs/authentication/api-keys — what an API key is and is not (last validated 2026-09-03)