00:00

API Gateway Security in Microservices

Last updated:

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

How a secure API gateway checks a requestA request from a client on the internet enters the API gateway, which runs its checks in order. Step 1 terminates TLS and decrypts the request. Step 2 applies a rate limit per client and rejects excess requests with 429 Too Many Requests. Step 3 verifies the token's signature, expiry, issuer and audience and rejects bad tokens with 401 Unauthorized. Step 4 strips client-supplied X-User headers and sets identity headers from the verified token. Step 5 applies the route rule on the normalised path and rejects forbidden requests with 403 Forbidden. Allowed requests go over mutual TLS to the Order Service, which verifies the token again and checks data ownership, returning 403 if the user is not the owner. Client internet API Gateway: checks run in this order 1. TLS decrypt 2. Rate limit per client 429 Too Many Requests 3. Verify token sig, exp, iss, aud 401 Unauthorized 4. Identity strip X-User-* 5. Route rule normalised path 403 Forbidden mTLS Order Service verifies token again, checks data ownership (403 if not owner)
Figure 1: the gateway runs cheap checks first and rejects early; the service still makes the final, data-level decision.

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":

JwtGatewayFilter.java
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));
    }
}
Output
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.

HeaderSanitizer.java
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));
    }
}
Output
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:

PathNormalization.java
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");
        }
    }
}
Output
/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

CheckAt the gatewayIn each service
TLS from clientsYes, terminated heremTLS between services
Token validationYes, rejects bad tokens earlyYes, again: never trust the network alone
Role-based route rulesYes, coarse rules per pathYes, per operation
Data ownership ("only your orders")No, it does not have the dataYes
Rate limiting and request sizeYesOptional, for expensive operations
Input validation of business fieldsBasic format onlyYes, 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 Authorization headers 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.