AllowedHosts
Restrict which Host headers the app answers with AllowedHosts, avoid breaking health probes, and decide between host filtering in the app and at the proxy.
"AllowedHosts": "api.example.com;*.shop.example.com" | AllowedHosts=*- Added in .NET ASP.NET Core 2.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$AllowedHostsoptional | string (hosts separated by ;)= unset: all hosts allowed; the templates set * | Exact names, *.domain for subdomains (not the bare domain), or * for any host. Punycode for internationalised names. Ports are ignored. |
$HostFilteringOptions.AllowEmptyHostsoptional | bool= true | Accept requests with no or empty Host header (HTTP/1.0). |
$HostFilteringOptions.IncludeFailureMessageoptional | bool= true | Write the HTML error body on rejection. |
Return value
stringA semicolon-separated list of host names (without ports) the host filtering middleware accepts. Any other Host header gets 400 Bad Request with an "Invalid Hostname" HTML body.
Errors
Nothing at startup. Rejected requests get 400 and are logged at Information by Microsoft.AspNetCore.HostFiltering (The host '...' does not match an allowed host), which the usual Microsoft.AspNetCore: Warning rule hides.
Both WebApplication.CreateBuilder and CreateSlimBuilder register host filtering middleware at the very start of the pipeline. It reads AllowedHosts from configuration and compares the request's Host header against it. A mismatch ends the request with 400 and a small HTML page titled "Bad Request - Invalid Hostname" before routing, authentication or your endpoints run. The templates put "AllowedHosts": "*" in appsettings.json, which turns the check off.
Why it exists
Code that builds absolute URLs from Request.Host (password-reset links, Location headers, LinkGenerator.GetUriByName) trusts whatever the client sent. If the app is reachable directly, an attacker can send Host: evil.example and get links pointing at their domain into emails or caches. Kestrel does not check host names at all, so AllowedHosts is the in-app defence. IIS and many reverse proxies already filter by host; when the ingress only routes known hosts to the app, the app-level check is defence in depth.
Matching rules
| Entry | Allows | Rejects |
|---|---|---|
api.example.com | api.example.com, api.example.com:8443 | www.example.com |
*.shop.example.com | eu.shop.example.com | shop.example.com (list it separately) |
* | Everything | Nothing |
| (empty Host, HTTP/1.0) | Allowed while AllowEmptyHosts is true |
The production failure
Setting AllowedHosts to the public name breaks every caller that does not use it. Kubernetes HTTP probes send the pod IP as the Host (10.1.4.23:8080), so liveness fails and the pod restarts in a loop. Internal callers that use the Service name (orders-api.shop.svc.cluster.local) also get 400. Rejections are logged at Information under Microsoft.AspNetCore.HostFiltering, and the usual Microsoft.AspNetCore: Warning rule hides them, so the symptom is a mysterious 400 with no log line. Either list every name the app is legitimately reached by, set a Host header on the probe (httpGet.httpHeaders), or leave * and enforce host names at the ingress. Behind a proxy, the Host the middleware sees is what the proxy forwarded; it only uses X-Forwarded-Host if forwarded headers are configured for it and run first.
Example
When to use it
- Stop password-reset emails from carrying attacker-supplied domains when the API builds absolute links from Request.Host
- Diagnose a CrashLoopBackOff where liveness probes get 400 Invalid Hostname after AllowedHosts was set to the public name
- Allow every tenant subdomain with *.shop.example.com while still listing shop.example.com for the bare domain
Frequently asked questions
Does *.example.com allow example.com?
No. The wildcard matches subdomains only. List example.com as a separate entry.
Why do I get 400 Bad Request with no log?
Host filtering rejected the Host header and logged it at Information, which the usual Microsoft.AspNetCore: Warning rule hides. Set Logging:LogLevel:Microsoft.AspNetCore.HostFiltering to Information, then compare the Host your caller sends (pod IP, Service name, public name) against AllowedHosts.
Should I set AllowedHosts if my ingress already routes by host?
It is defence in depth for when the app is reached some other way. If you set it, list internal Service names and give probes a Host header; otherwise * plus strict ingress rules is a reasonable choice.
More examples
appsettings.Production.json listing every real name
Every name a legitimate caller uses must be listed: the public host, the Service DNS names other pods use, and any custom domain. The pod IP is deliberately absent, so the probes in the next example send a Host header.
{
// Public name, the in-cluster Service name, and the short Service name
"AllowedHosts": "api.shop.example.com;orders-api.shop.svc.cluster.local;orders-api"
}Probes that send a valid Host header
Without <code>httpHeaders</code> the kubelet sends the pod IP and port as Host, which a strict list rejects with 400, and a failing liveness probe restarts the container. Setting the header keeps host filtering on without carving out exceptions in code.
apiVersion: v1
kind: Pod
metadata:
name: orders-api
spec:
containers:
- name: api
image: registry.example.com/orders-api:1.8.2
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
httpHeaders:
- name: Host
value: orders-api
readinessProbe:
httpGet:
path: /health/ready
port: 8080
httpHeaders:
- name: Host
value: orders-api
Tightening the options in code
The list itself still comes from the <code>AllowedHosts</code> key; these options change behaviour around it. Rejected requests are logged at Information by the <code>Microsoft.AspNetCore.HostFiltering</code> category, so add a rule setting that category to Information when you investigate unexplained 400s.
using Microsoft.AspNetCore.HostFiltering;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<HostFilteringOptions>(options =>
{
// HTTP/1.0 clients without a Host header are not expected for this API.
options.AllowEmptyHosts = false;
// Plain 400 with no HTML body for API clients.
options.IncludeFailureMessage = false;
});
var app = builder.Build();
app.MapGet("/products/{sku}", (string sku) => TypedResults.Ok(new { sku, price = 24.90m }));
app.Run();
Discussion