HTTP Strict Transport Security is one of those headers that looks trivial until you ship it wrong and lock users into a bad config for months.
If you run apps on Azure or GCP, the main challenge usually is not the header itself. It’s figuring out where to set it so it’s applied consistently, survives redirects, and doesn’t get stripped by a proxy, CDN, or app server. This guide is the practical version: what to send, where to send it, and copy-paste examples.
The header you actually want
A typical production HSTS header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
If you want to be considered for browser preload lists, use:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
What the directives mean
max-age=31536000— tells browsers to remember HTTPS-only for 1 yearincludeSubDomains— applies the policy to all subdomainspreload— signals intent for browser preload programs
My advice: don’t jump straight to preload unless you’re very sure every current and future subdomain can serve HTTPS correctly.
Safe rollout strategy
I’ve seen teams go straight to one year, then discover some forgotten subdomain still serves plain HTTP. That gets ugly fast.
Use a staged rollout:
Strict-Transport-Security: max-age=300
Then:
Strict-Transport-Security: max-age=86400
Then:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Only after you’ve validated everything should you consider:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Rules that matter in production
- Only send HSTS over HTTPS. Browsers ignore it on HTTP.
- Redirect HTTP to HTTPS before anything else.
- Set it at the edge when possible. That gives you consistency across apps.
- Don’t use
includeSubDomainscasually. - Don’t preload unless you’re ready for the commitment.
If you want a quick check after deployment, run a scan at headertest.com.
Azure
Azure App Service
If your app runs directly on App Service, you can set HSTS in the application itself or in web server config. I generally prefer app-level config when the stack supports it cleanly, because it stays with the app. If multiple apps sit behind a shared edge, I prefer edge config.
ASP.NET Core
In Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.MapGet("/", () => "Hello over HTTPS");
app.Run();
Customize it:
using Microsoft.AspNetCore.HttpsPolicy;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = false;
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.Run();
IIS on App Service with web.config
For Windows App Service hosting classic ASP.NET or IIS-backed apps:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
</customHeaders>
</httpProtocol>
<rewrite>
<rules>
<rule name="HTTP to HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
Node.js / Express on App Service
Use Helmet:
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: false
}));
app.get('/', (req, res) => {
res.send('Hello over HTTPS');
});
app.listen(process.env.PORT || 3000);
If App Service terminates TLS upstream, make sure Express trusts the proxy so HTTPS redirects and secure behavior work correctly:
app.set('trust proxy', 1);
Azure Front Door
If you use Front Door, this is one of the best places to inject HSTS because it applies consistently at the edge.
Using Rule Set behavior, add or overwrite the response header:
- Header name:
Strict-Transport-Security - Value:
max-age=31536000; includeSubDomains
You can also do this with Azure CLI once your rule set exists.
Example pattern:
az afd rule action add \
--resource-group my-rg \
--profile-name my-frontdoor \
--rule-set-name security-headers \
--rule-name add-hsts \
--action-name ModifyResponseHeader \
--header-action Overwrite \
--header-name Strict-Transport-Security \
--value "max-age=31536000; includeSubDomains"
Then make sure HTTP is redirected to HTTPS in your route configuration.
What I like about Front Door for HSTS:
- one place to manage it
- less drift across services
- easy to standardize across multiple backends
What I don’t like:
- teams sometimes forget the backend also emits a different HSTS value, and now debugging becomes annoying
Pick one source of truth if you can.
Azure Application Gateway
Application Gateway can rewrite response headers. Add a rewrite rule set that injects HSTS:
az network application-gateway rewrite-rule set create \
--resource-group my-rg \
--gateway-name my-appgw \
--name securityHeaders
Then add a rewrite rule and response header configuration:
az network application-gateway rewrite-rule create \
--resource-group my-rg \
--gateway-name my-appgw \
--rule-set-name securityHeaders \
--name addHsts \
--sequence 100
az network application-gateway rewrite-rule action set \
--resource-group my-rg \
--gateway-name my-appgw \
--rule-set-name securityHeaders \
--rule-name addHsts \
--response-headers "Strict-Transport-Security=max-age=31536000; includeSubDomains"
Also configure an HTTP listener and redirect configuration so plain HTTP never serves content.
Azure CDN / edge cases
If you’re using Azure CDN or a Microsoft edge service in front of your app, verify whether the product supports response header modification in your chosen SKU. This changes over time, and Azure has a habit of making capability matrices more confusing than they need to be.
If the edge can set the header, do it there. If not, set it in the origin app and verify the CDN forwards it unchanged.
Official docs worth checking:
- Azure App Service documentation
- Azure Front Door documentation
- Azure Application Gateway documentation
GCP
Cloud Load Balancing with backend services
On GCP, the cleanest place to add HSTS is often the external HTTP(S) Load Balancer using custom response headers on the backend service.
Example with gcloud:
gcloud compute backend-services update my-backend-service \
--global \
--custom-response-header="Strict-Transport-Security: max-age=31536000; includeSubDomains"
If you use regional load balancing, use the regional variant of the command.
Then create or verify an HTTP-to-HTTPS redirect. For classic global external Application Load Balancers, the redirect is usually configured on the URL map and target proxy setup.
The nice thing here is the same as Front Door: one edge policy, many backends.
Cloud Run
If Cloud Run sits behind a GCP load balancer, I prefer setting HSTS at the load balancer. If Cloud Run is exposed more directly, set it in the app.
Node.js / Express on Cloud Run
const express = require('express');
const helmet = require('helmet');
const app = express();
app.set('trust proxy', 1);
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: false
}));
app.get('/', (req, res) => {
res.send('Hello from Cloud Run');
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Listening on ${port}`);
});
Python / Flask on Cloud Run
from flask import Flask
app = Flask(__name__)
@app.after_request
def add_hsts(response):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
@app.route("/")
def hello():
return "Hello from Cloud Run"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
GKE with NGINX Ingress
If you run GKE with NGINX Ingress, this is easy.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
The always flag matters. Without it, some responses may miss the header.
GCE VM with NGINX
For Compute Engine VMs running NGINX:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
GCE VM with Apache
<VirtualHost *:443>
ServerName example.com
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
Make sure the required modules are enabled:
a2enmod headers
a2enmod proxy
a2enmod proxy_http
Common mistakes
Sending HSTS on HTTP
Does nothing. Browsers ignore it.
Setting different HSTS values at multiple layers
App says 1 day, edge says 1 year, old CDN rule says 6 months. Now nobody knows what users cached. Keep it simple.
Using includeSubDomains too early
That old dev. or status. hostname will come back to haunt you.
Preloading before you’re ready
Preload is not a fun rollback story.
Forgetting redirects
HSTS helps after the browser has seen the policy. The first visit still needs a proper HTTPS redirect unless the domain is preloaded.
How to verify
Use curl against the HTTPS endpoint:
curl -I https://example.com
You want to see:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Check the HTTP redirect too:
curl -I http://example.com
You want a 301 or 308 redirect to HTTPS.
For a broader header check, scan the site with headertest.com.
Official documentation
Azure:
- Azure App Service documentation
- Azure Front Door documentation
- Azure Application Gateway documentation
GCP:
- Google Cloud Load Balancing documentation
- Cloud Run documentation
- Google Kubernetes Engine documentation
If I had to boil this down to one opinionated rule: set HSTS at the edge when you can, set it in the app when you must, and roll it out like you expect one forgotten subdomain to ruin your day.