API Gateway Security in Microservices
API gateway security means protecting the single entry point of a microservices system, so that every request from the internet is checked before it reaches any service. A secure gateway encrypts traffic, verifies who the caller is, limits how often they can call, and blocks requests that should never get through. Because all external traffic passes through it, it is the most effective place for shared security checks.
This tutorial explains what the gateway should check and in which order, what must stay inside each service, and how attackers get around weak gateways. Three runnable Java examples show token validation, header sanitising and path normalisation. Interview questions follow at the end.
1. What Is API Gateway Security?
An API gateway is a server that sits between clients and your microservices. Clients call the gateway, and it routes each request to the right service. API gateway security is the set of checks the gateway runs on every request before routing it: encryption, authentication (proving who the caller is), authorization (deciding what they may do), rate limiting and request validation. The authentication vs authorization guide explains the difference in detail.
2. Why Secure the Gateway, and Why It Is Not Enough
Without a gateway, every service must handle tokens, rate limits and TLS on its own, and one forgotten check exposes the whole system. The gateway does these shared checks once, in one place, for every route.
But the gateway is only the outer wall. If an attacker reaches a service directly, for example through a misconfigured network or another compromised service, the gateway's checks never run. Services must still verify identity and enforce their own business rules. This layered approach is called defence in depth.
3. How a Secure API Gateway Handles a Request
The order matters. Cheap checks that stop abuse, such as TLS and rate limiting, run first, so attackers cannot make the gateway do expensive work like signature verification millions of times. Only a request that passes every step is forwarded, and the service still makes the final, detailed decision.
4. Core API Gateway Security Controls
4.1 TLS Everywhere
TLS encrypts traffic so nobody on the network can read or change it. The gateway terminates TLS from clients. Traffic from the gateway to services should be encrypted too, often with mutual TLS (mTLS), where both sides present certificates.
4.2 Authentication with Tokens
Most gateways expect an access token, usually a JSON Web Token (JWT) issued by an authorization server through OAuth 2.0. The gateway checks the token's signature, expiry, issuer and audience. In Spring Cloud Gateway this is the OAuth2 resource server support: you point spring.security.oauth2.resourceserver.jwt.issuer-uri at your authorization server, and Spring downloads its public keys.
4.3 Coarse-Grained Authorization
The gateway can enforce simple rules per route, such as "only admins may call /api/admin/**". Rules that depend on data, such as "customers may only see their own orders", belong in the service that owns that data.
4.4 Rate Limiting
Limiting requests per client protects services from overload and slows down password guessing. The rate limiting pattern covers the algorithms.
4.5 Request Validation
Reject oversized bodies, unexpected methods and malformed headers early. Configure CORS (which browser origins may call the API) once at the gateway instead of in every service.
4.6 Hiding Internal Endpoints
Management endpoints such as /actuator or internal admin APIs should simply have no public route. A route that does not exist cannot be attacked from outside.
5. API Gateway Security in Java: Examples
These programs run on Java 21 using only the JDK, so you can see what happens inside. In production, use a tested library such as Spring Security instead of writing token checks yourself.
5.1 Validating a Signed Token
The gateway rejects a token whose role was changed, an expired token, and an unsigned token that claims "alg":"none":
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class JwtGatewayFilter {
static final byte[] SECRET = "change-me-to-a-long-random-secret-32b".getBytes(StandardCharsets.UTF_8);
static final String EXPECTED_ISSUER = "https://auth.shop.example";
static final String EXPECTED_AUDIENCE = "shop-api";
static final Base64.Encoder B64 = Base64.getUrlEncoder().withoutPadding();
static final Base64.Decoder B64D = Base64.getUrlDecoder();
static String hmac(String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET, "HmacSHA256"));
return B64.encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
// Issued by the authorization server (shown here only to create test tokens)
static String issue(String alg, String sub, String role, long exp) throws Exception {
String header = B64.encodeToString(("{\"alg\":\"" + alg + "\",\"typ\":\"JWT\"}").getBytes(StandardCharsets.UTF_8));
String payload = B64.encodeToString(("{\"sub\":\"" + sub + "\",\"role\":\"" + role + "\",\"iss\":\"" + EXPECTED_ISSUER
+ "\",\"aud\":\"" + EXPECTED_AUDIENCE + "\",\"exp\":" + exp + "}").getBytes(StandardCharsets.UTF_8));
return header + "." + payload + "." + hmac(header + "." + payload);
}
static String claim(String json, String name) {
Matcher m = Pattern.compile("\"" + name + "\":\"?([^\",}]*)").matcher(json);
return m.find() ? m.group(1) : null;
}
// What the gateway does for every request
static String check(String token, Instant now) throws Exception {
String[] parts = token.split("\\.", -1); // -1 keeps an empty signature part
if (parts.length != 3) return "401 malformed token";
String header = new String(B64D.decode(parts[0]), StandardCharsets.UTF_8);
String payload = new String(B64D.decode(parts[1]), StandardCharsets.UTF_8);
if (!"HS256".equals(claim(header, "alg"))) return "401 unexpected algorithm " + claim(header, "alg"); // never trust "none"
boolean signatureOk = MessageDigest.isEqual( // constant-time compare
hmac(parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8), parts[2].getBytes(StandardCharsets.UTF_8));
if (!signatureOk) return "401 bad signature";
if (Long.parseLong(claim(payload, "exp")) <= now.getEpochSecond()) return "401 token expired";
if (!EXPECTED_ISSUER.equals(claim(payload, "iss"))) return "401 wrong issuer";
if (!EXPECTED_AUDIENCE.equals(claim(payload, "aud"))) return "401 wrong audience";
return "200 forward as user " + claim(payload, "sub") + " (role " + claim(payload, "role") + ")";
}
public static void main(String[] args) throws Exception {
Instant now = Instant.parse("2026-10-28T10:00:00Z");
long inOneHour = now.plusSeconds(3600).getEpochSecond();
String valid = issue("HS256", "asha", "CUSTOMER", inOneHour);
String[] p = valid.split("\\.");
String tamperedPayload = B64.encodeToString(new String(B64D.decode(p[1]), StandardCharsets.UTF_8)
.replace("CUSTOMER", "ADMIN").getBytes(StandardCharsets.UTF_8));
String tampered = p[0] + "." + tamperedPayload + "." + p[2]; // role changed, old signature kept
String expired = issue("HS256", "asha", "CUSTOMER", now.minusSeconds(60).getEpochSecond());
String noneAlg = B64.encodeToString("{\"alg\":\"none\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8))
+ "." + p[1] + "."; // unsigned token
System.out.println("valid: " + check(valid, now));
System.out.println("tampered: " + check(tampered, now));
System.out.println("expired: " + check(expired, now));
System.out.println("alg none: " + check(noneAlg, now));
}
}
valid: 200 forward as user asha (role CUSTOMER)
tampered: 401 bad signature
expired: 401 token expired
alg none: 401 unexpected algorithm none
Two details matter here. The gateway decides the algorithm itself instead of trusting the token header, and it compares signatures in constant time with MessageDigest.isEqual, so response timing reveals nothing.
5.2 Stripping Forged Identity Headers
Gateways often tell services who the user is through headers such as X-User-Id. Most tutorials skip the next step: the gateway must remove those headers when they arrive from the client, or anyone can claim to be an admin.
import java.util.LinkedHashMap;
import java.util.Map;
public class HeaderSanitizer {
// Identity the gateway extracted from a token it has already verified
record VerifiedUser(String id, String role) {}
static Map<String, String> toDownstream(Map<String, String> incoming, VerifiedUser user) {
Map<String, String> headers = new LinkedHashMap<>();
incoming.forEach((name, value) -> {
// Drop identity headers a client could forge; keep the token so services can verify it too
if (!name.toLowerCase().startsWith("x-user-")) {
headers.put(name, value);
}
});
headers.put("X-User-Id", user.id()); // set only by the gateway
headers.put("X-User-Role", user.role());
return headers;
}
public static void main(String[] args) {
Map<String, String> fromInternet = new LinkedHashMap<>();
fromInternet.put("Accept", "application/json");
fromInternet.put("Authorization", "Bearer eyJhbGciOi...");
fromInternet.put("X-User-Id", "admin"); // forged by the client
fromInternet.put("x-user-role", "ADMIN"); // forged, different case
VerifiedUser fromToken = new VerifiedUser("asha", "CUSTOMER");
System.out.println("Client sent: " + fromInternet);
System.out.println("Service receives: " + toDownstream(fromInternet, fromToken));
}
}
Client sent: {Accept=application/json, Authorization=Bearer eyJhbGciOi..., X-User-Id=admin, x-user-role=ADMIN}
Service receives: {Accept=application/json, Authorization=Bearer eyJhbGciOi..., X-User-Id=asha, X-User-Role=CUSTOMER}
The forged lower-case x-user-role is caught too, because HTTP header names are case-insensitive.
5.3 Normalising Paths Before Checking Rules
A rule checked against the raw path can be bypassed with .. segments or their URL-encoded form, %2e%2e. The service behind the gateway may resolve them, but the naive check does not:
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
public class PathNormalization {
// Rule: only admins may call /api/admin/**
static boolean needsAdmin(String path) {
return path.startsWith("/api/admin/");
}
// Decode %xx escapes first, then resolve "." and ".." segments
static String canonical(String rawPath) throws URISyntaxException {
String decoded = new URI(rawPath).getPath();
return new URI(null, null, decoded, null).normalize().getPath();
}
public static void main(String[] args) throws URISyntaxException {
List<String> requests = List.of(
"/api/admin/users",
"/api/orders/../admin/users",
"/api/orders/%2e%2e/admin/users");
for (String raw : requests) {
String path = canonical(raw);
System.out.printf("%-32s naive check: %-6s canonical %-18s check: %s%n",
raw, needsAdmin(raw) ? "ADMIN" : "open", path, needsAdmin(path) ? "ADMIN" : "open");
}
}
}
/api/admin/users naive check: ADMIN canonical /api/admin/users check: ADMIN
/api/orders/../admin/users naive check: open canonical /api/admin/users check: ADMIN
/api/orders/%2e%2e/admin/users naive check: open canonical /api/admin/users check: ADMIN
Frameworks such as Spring Security reject many suspicious paths by default, but any custom filter or route rule must match on the decoded, normalised path.
6. Gateway vs Service: Who Checks What
| Check | At the gateway | In each service |
|---|---|---|
| TLS from clients | Yes, terminated here | mTLS between services |
| Token validation | Yes, rejects bad tokens early | Yes, again: never trust the network alone |
| Role-based route rules | Yes, coarse rules per path | Yes, per operation |
| Data ownership ("only your orders") | No, it does not have the data | Yes |
| Rate limiting and request size | Yes | Optional, for expensive operations |
| Input validation of business fields | Basic format only | Yes, full validation |
7. Common Mistakes and Pitfalls
- Trusting the internal network. Services that accept any request without a token are exposed the moment one internal component is compromised.
- Forwarding client identity headers. As in section 5.2, headers from outside must be removed, not passed on.
- Matching rules on raw paths. Encoded or
..paths slip past string checks (section 5.3). - Skipping audience and issuer checks. A valid token issued for a different application should not open your API.
- Long-lived tokens. A stolen token that lasts a week gives an attacker a week. Keep access tokens short and use refresh tokens.
- Logging secrets. Request logs must never contain
Authorizationheaders or full tokens.
8. Best Practices
- Run cheap checks first: TLS, size limits and rate limits before token verification.
- Validate signature, expiry, issuer and audience on every token, with an allow-list of algorithms.
- Strip incoming identity headers and set them only from the verified token.
- Normalise paths before applying any route rule.
- Keep services secure on their own: verify tokens and enforce data-level rules there too.
- Lock down the network so services accept traffic only from the gateway and each other.
- Use a proven framework, such as Spring Security in Spring Cloud Gateway, instead of hand-written crypto.
9. Summary
- API gateway security checks every external request at one entry point before it reaches a service.
- Core controls are TLS, token authentication, coarse authorization, rate limiting, request validation and hiding internal endpoints.
- The gateway must strip forged identity headers and match rules on normalised paths.
- The gateway is the outer wall only: services still verify tokens and enforce data ownership.
- Good API gateway security combines a hardened gateway with services that never trust the network.
API Gateway Security: Interview Questions
Common interview questions on securing an API gateway, from basic to scenario-based, with short answers.
Basic questions
Q. What is the difference between 401 and 403 at the gateway?
Answer: 401 Unauthorized means the gateway does not know who you are: the token is missing, expired or invalid. 403 Forbidden means the gateway knows who you are, but you are not allowed to do this, for example a customer calling an admin route.
Q. Should the gateway or the services validate JWTs?
Answer: Both. The gateway rejects bad tokens early and saves services from useless work, but services must not assume every request came through the gateway. Validating a JWT is cheap because it is a local signature check, so doing it twice costs little.
Q. What is an API key, and is it enough to secure an API?
Answer: An API key is a long secret string that identifies a calling application. It is fine for identifying partners or for simple server-to-server use with rate limits. It is not enough for user-facing APIs, because it does not identify the user, rarely expires and is easy to leak. Use OAuth 2.0 access tokens for users.
Intermediate questions
Q. JWT or opaque token: what changes at the gateway?
Answer: A JWT carries its claims and can be validated locally with the issuer's public key. An opaque token is just a random reference, so the gateway must call the authorization server's introspection endpoint to learn who it belongs to. Opaque tokens are easier to revoke but add a network call, which gateways usually reduce by caching introspection results briefly.
Q. How do you revoke a JWT before it expires?
Answer: A JWT stays valid until its exp time because nobody checks a central list. Keep access tokens short-lived (minutes), revoke the refresh token so no new access tokens are issued, and for urgent cases keep a small denylist of revoked token IDs (jti) at the gateway.
Q. How do you configure CORS at the gateway?
Answer: Allow only the browser origins that need the API, for example https://shop.example, and only the methods and headers they use. Never combine a wildcard origin * with credentials, which browsers reject anyway. Configure CORS once at the gateway so services do not add conflicting headers.
Q. What is token relay?
Answer: Token relay means the gateway forwards the user's access token to the downstream service, so the service can verify it and see the user's identity and scopes. Spring Cloud Gateway provides a TokenRelay filter for this when the gateway logs users in with OAuth 2.0.
Q. How do you protect Actuator and other internal endpoints?
Answer: Do not create public gateway routes for them. In Spring Boot you can also run them on a separate port with management.server.port, expose only what you need with management.endpoints.web.exposure.include, and allow that port only from your monitoring network.
Tricky and scenario questions
Q. A service is reachable directly, bypassing the gateway. How do you fix it?
Answer: Put services on a private network with no public address, and use network rules (security groups, Kubernetes NetworkPolicies) so they accept traffic only from the gateway and other services. Add mTLS between services, and make each service verify tokens, so even a direct request without a valid token is rejected.
Q. An admin removed a user's ADMIN role, but the user can still call admin routes. Why?
Answer: The user's existing JWT still contains the old role, and the gateway trusts the claims until the token expires. Shorter token lifetimes reduce the window; for sensitive actions, the service can check current permissions in its own database instead of relying only on token claims.
Q. Can rate limiting at the gateway stop a DDoS attack?
Answer: Only small ones. A large distributed attack can overwhelm the gateway's network and CPU before any limit applies. Use an upstream DDoS protection or CDN service for volume attacks, and keep gateway rate limits for abuse by individual clients and keys.
Q. Why must the gateway check the token's audience?
Answer: An organisation's authorization server often issues tokens for many applications. Without an audience (aud) check, a valid token issued for a harmless reporting app could be replayed against your payment API. Checking that aud contains your API's identifier keeps each token usable only where it was meant to be used.
Q. Is it safe for the gateway to log request headers for debugging?
Answer: Only after removing secrets. Authorization headers, cookies and API keys must be masked or dropped, because anyone who can read the logs could reuse them. Log a correlation ID and the user ID instead, which is enough to trace a request.
Q. How does a Backend for Frontend (BFF) improve browser security?
Answer: With a BFF, the browser never holds access tokens. It keeps only a secure, HTTP-only session cookie, and the BFF stores the tokens server-side and attaches them to API calls. This protects tokens from being stolen by malicious JavaScript, a common risk when single-page apps store tokens in the browser.