HTTP Strict Transport Security sounds simple: send one response header, force browsers onto HTTPS, done. In practice, the hard part is deciding where to manage it in Kong and how aggressively to roll it out without breaking edge cases.
If you run Kong API Gateway, you usually have a few choices:
- Set HSTS in Kong itself
- Set HSTS in an upstream app behind Kong
- Set HSTS at an outer load balancer or CDN instead of Kong
- Use a phased rollout with short
max-agefirst, then increase later
I’m opinionated on this one: if Kong is your main HTTPS entry point, HSTS usually belongs there. It keeps policy consistent across services and avoids every team reinventing the same header logic. But there are trade-offs, and some teams absolutely should not flip on preload-style settings on day one.
Quick HSTS refresher
The header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Common directives:
max-age=31536000— browsers remember HTTPS-only for one yearincludeSubDomains— apply policy to all subdomainspreload— request inclusion in browser preload lists, which is a much bigger commitment
HSTS only works when the browser sees it over a valid HTTPS connection. Sending it over plain HTTP does nothing useful.
For API gateways, one caveat: HSTS is a browser control. It protects browser-based clients, not arbitrary backend integrations, mobile SDKs, or server-to-server calls that ignore browser transport rules.
Option 1: Manage HSTS in Kong
This is the cleanest option for most teams.
How to do it
With Kong, a common pattern is to inject the header at the gateway layer using response transformation or server/header configuration depending on your deployment model.
Example with the response-transformer plugin:
curl -s -X POST http://localhost:8001/plugins \
--data "name=response-transformer" \
--data "config.add.headers=Strict-Transport-Security:max-age=31536000; includeSubDomains"
If you only want it on a specific service or route:
curl -s -X POST http://localhost:8001/services/my-service/plugins \
--data "name=response-transformer" \
--data "config.add.headers=Strict-Transport-Security:max-age=86400"
Declarative config example:
_format_version: "3.0"
services:
- name: my-api
url: http://upstream-api:8080
routes:
- name: my-api-route
paths:
- /api
plugins:
- name: response-transformer
config:
add:
headers:
- "Strict-Transport-Security:max-age=31536000; includeSubDomains"
You can verify the result with curl:
curl -I https://api.example.com
Expected output:
HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains
You can also run a quick scan with Headertest to confirm the header is actually reaching clients.
Pros
- Centralized policy: one place to manage transport security behavior
- Consistent across services: no guessing which app forgot to add the header
- Good fit for platform teams: app teams don’t need to own low-level header policy
- Easy phased rollout: start with short
max-age, then increase safely
Cons
- Can hide app-level complexity: some apps may still generate redirects or mixed assumptions you haven’t fixed
- Risk of duplicate headers: if upstreams also send HSTS, you need to decide who owns it
- Per-route inconsistency is easy to create: route-level plugins can drift if you’re not disciplined
Best fit
Use this when Kong is the primary public HTTPS boundary and you want one security posture across many APIs.
Option 2: Manage HSTS in the upstream application
This is common when teams fully own their apps and Kong is treated as a thin proxy.
Example in Node/Express:
app.use((req, res, next) => {
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
next();
});
Example in NGINX behind Kong:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Pros
- App teams keep ownership of security behavior
- Closer to business logic if some responses genuinely need different treatment
- Portable if the app also runs outside Kong in another environment
Cons
- Inconsistent rollout across teams and services
- Easy to miss on legacy services
- Harder to audit than one gateway config
- Duplicate ownership when Kong or an outer proxy also injects headers
Best fit
Use this if your organization is highly decentralized and each app already owns its own edge behavior. I still think this is weaker operationally for shared API platforms.
Option 3: Manage HSTS at the CDN or external load balancer
Sometimes Kong sits behind another HTTPS layer like a cloud load balancer, ingress controller, or CDN. In that case, you can set HSTS there instead.
Pros
- Applies before traffic reaches Kong
- Single policy for many origins
- Good for organizations with a strict edge security model
Cons
- Kong no longer reflects final client-facing policy in its own config
- Can get confusing fast when troubleshooting
- Environment drift is common between staging, production, and regional edges
Best fit
Use this when the outer edge is the true public boundary and Kong is effectively internal. If browsers never directly see Kong’s TLS endpoint, outer-edge ownership makes sense.
Comparison: which layer should own HSTS?
Here’s the blunt version.
Kong-owned HSTS
Pros
- Centralized
- Consistent
- Easy for platform teams
- Strong default for shared gateways
Cons
- Needs coordination with upstream headers
- Can create false confidence if HTTPS redirects and cert coverage are messy
App-owned HSTS
Pros
- Team autonomy
- Portable config
- Fine-grained control
Cons
- Inconsistent
- Hard to audit
- More chances to forget it
CDN/LB-owned HSTS
Pros
- Closest to the real browser-facing edge
- Broad coverage
- Good for large edge-managed estates
Cons
- Less visible in Kong
- More moving parts
- Debugging can be annoying
If you want one recommendation: own HSTS at the outermost browser-facing HTTPS layer. If that layer is Kong, configure it in Kong. If that layer is your CDN, do it there. Don’t spread ownership across all three.
Rollout strategy: the part people rush
The biggest HSTS mistake is not technical. It’s turning on a long max-age before you’ve verified every relevant hostname is cleanly HTTPS-only.
A safer rollout for Kong looks like this:
Phase 1: Start small
curl -s -X POST http://localhost:8001/plugins \
--data "name=response-transformer" \
--data "config.add.headers=Strict-Transport-Security:max-age=300"
That’s 5 minutes. Enough to test behavior without locking users in for months.
Phase 2: Increase after validation
Move to one day:
Strict-Transport-Security: max-age=86400
Then one month:
Strict-Transport-Security: max-age=2592000
Then one year:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Phase 3: Consider preload only if you really mean it
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
preload is not a casual checkbox. You’re declaring that the whole domain tree is HTTPS-only and should stay that way. If you still have oddball subdomains, old admin panels, or forgotten non-TLS services, don’t do this yet.
Common mistakes with Kong and HSTS
1. Sending HSTS on HTTP listeners
If someone can hit Kong on port 80 and get a plain HTTP response with HSTS, that header doesn’t buy you much. You still need clean HTTP-to-HTTPS redirects and a proper TLS-first posture.
2. Using includeSubDomains too early
This breaks forgotten subdomains faster than people expect. Inventory first, then enable.
3. Assuming HSTS protects API clients universally
It helps browsers. It does not magically fix insecure integrations that hardcode http:// or skip certificate validation.
4. Duplicating the header in multiple layers
Kong, upstream NGINX, and CDN all setting different HSTS values is a classic mess. Pick an owner.
5. Preloading before certificate coverage is complete
If every subdomain isn’t consistently HTTPS with valid certs, preload is reckless.
Practical recommendation
For most Kong deployments, I’d do this:
- Make sure Kong is only serving public traffic over valid HTTPS
- Redirect HTTP to HTTPS cleanly
- Set HSTS in Kong with a short
max-age - Validate all relevant routes and subdomains
- Increase to one year
- Add
includeSubDomainsonly after inventory and testing - Treat
preloadas a separate project, not a default
A solid starting header for many teams is:
Strict-Transport-Security: max-age=86400
A mature production header is usually:
Strict-Transport-Security: max-age=31536000; includeSubDomains
And only after serious validation:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
For Kong-specific behavior and plugin options, check the official Kong docs:
If you want a fast sanity check after rollout, run your hostname through Headertest. I like doing that right after a gateway change because it catches the obvious “configured but not actually returned” mistake in seconds.