How PI_WEB_ALLOWED_HOSTS Configures Trusted Proxy Hostnames in pi-web
PI_WEB_ALLOWED_HOSTS is a comma-separated environment variable that extends the trusted hostname list in pi-web, allowing specific reverse-proxies to forward API requests by validating the HTTP Host header against a normalized allow-list.
The pi-web repository (agegr/pi-web) implements a strict security model for API routes that validates incoming hostnames to prevent DNS rebinding and host header injection attacks. When deploying behind reverse-proxies or load balancers, you must explicitly declare trusted hostnames using PI_WEB_ALLOWED_HOSTS, which integrates with the request security layer in lib/request-security.ts to authorize legitimate proxy traffic.
How PI_WEB_ALLOWED_HOSTS Works
The security validation pipeline processes environment-based hostnames through three distinct phases: ingestion from environment variables, normalization for canonical comparison, and runtime verification against incoming requests.
Reading the Environment Variable
The function configuredHostnamesFromEnvironment() located in lib/request-security.ts (lines 33-38) constructs the authoritative list of trusted hostnames by merging the singular PI_WEB_HOSTNAME value with entries parsed from PI_WEB_ALLOWED_HOSTS:
function configuredHostnamesFromEnvironment(): string[] {
return [
process.env.PI_WEB_HOSTNAME,
...(process.env.PI_WEB_ALLOWED_HOSTS?.split(",") ?? []),
].filter((value): value is string => Boolean(value?.trim()));
}
This implementation treats PI_WEB_ALLOWED_HOSTS as a comma-delimited string, splitting entries and filtering out empty or whitespace-only values while preserving the primary hostname defined in PI_WEB_HOSTNAME.
Normalizing Hostnames for Comparison
Before validation, each configured hostname undergoes canonicalization via normalizeConfiguredHostname() (lines 23-27). This process lower-cases the string, strips trailing dots and brackets, and preserves IP addresses in their literal form to ensure deterministic matching against the HTTP Host header regardless of casing variations.
Validating Incoming Requests
When the proxy middleware processes a request, isApiRequestHostAllowed() (lines 71-88) extracts the Host header and applies a tiered trust model. The function immediately permits loopback addresses (localhost, *.localhost) and raw IP literals, then iterates through the normalized configured hostnames to find a match:
export function isApiRequestHostAllowed(
request: Request,
configuredHostnames = configuredHostnamesFromEnvironment(),
): boolean {
const host = request.headers.get("host");
const hostname = host ? hostnameFromAuthority(host) : null;
if (!hostname) return false;
if (isLoopbackHostname(hostname) || isIP(hostname)) return true;
return configuredHostnames.some(
(configured) => normalizeConfiguredHostname(configured) === hostname,
);
}
If the normalized request hostname matches any entry derived from PI_WEB_ALLOWED_HOSTS, the function returns true, signaling that the proxy origin is trusted.
Proxy Middleware Integration
The central proxy handler in app/api/proxy.ts (lines 11-18 and 22-23) delegates trust decisions to isApiRequestAllowed(), which orchestrates hostname validation alongside CSRF-style origin checks. Requests failing the hostname verification receive an immediate HTTP 403 Forbidden response before reaching application business logic, ensuring that only explicitly configured proxies can access sensitive API endpoints.
Configuration Examples
Enabling Multiple Proxy Hostnames
To trust a reverse-proxy at proxy.example.com alongside an internal development host, set the environment variable as a comma-separated list:
# .env
PI_WEB_ALLOWED_HOSTS=proxy.example.com,api.internal.local
With this configuration, incoming requests containing Host: proxy.example.com or Host: api.internal.local pass validation, while requests from unknown hostnames are rejected.
Docker Compose Deployment
When containerizing pi-web, pass the allowed hosts via the environment section:
services:
pi-web:
image: ghcr.io/agegr/pi-web:latest
environment:
- PI_WEB_ALLOWED_HOSTS=reverse-proxy.internal,10.0.0.5
- PI_WEB_HOSTNAME=main.example.com
This explicitly permits both the DNS name reverse-proxy.internal and the IP literal 10.0.0.5. Note that IP literals are implicitly trusted even when not listed, but explicit declaration documents the intended architecture.
Runtime Verification
You can programmatically verify that your configuration parses correctly using the security utilities in a Node.js environment:
import { isApiRequestHostAllowed } from "./lib/request-security";
// Simulate environment configuration
process.env.PI_WEB_ALLOWED_HOSTS = "trusted-proxy.example.com";
process.env.PI_WEB_HOSTNAME = "api.example.com";
// Create a mock request
const request = new Request("http://localhost/api", {
headers: { host: "trusted-proxy.example.com" },
});
console.log(isApiRequestHostAllowed(request)); // Output: true
Summary
- PI_WEB_ALLOWED_HOSTS accepts a comma-separated list of hostnames that extend the default trust model defined by
PI_WEB_HOSTNAMEinlib/request-security.ts. - The validation logic resides in
configuredHostnamesFromEnvironment()for parsing andisApiRequestHostAllowed()for runtime verification against the HTTP Host header. - Hostnames undergo normalization (lowercasing, trimming trailing dots) before comparison to eliminate case-sensitivity bypasses.
- Loopback addresses (
localhost) and raw IP literals receive implicit trust regardless of configuration values. - The proxy middleware in
app/api/proxy.tsenforces these rules by returning HTTP 403 for requests originating from non-allowed hostnames.
Frequently Asked Questions
What is the difference between PI_WEB_HOSTNAME and PI_WEB_ALLOWED_HOSTS?
PI_WEB_HOSTNAME defines the primary canonical hostname for the application, while PI_WEB_ALLOWED_HOSTS provides a comma-separated list of additional trusted proxies. According to the source code in lib/request-security.ts, the configuredHostnamesFromEnvironment() function merges both values into a single allow-list, with PI_WEB_HOSTNAME serving as the required primary entry and PI_WEB_ALLOWED_HOSTS extending it for multi-proxy deployments.
Does PI_WEB_ALLOWED_HOSTS support wildcards or IP ranges?
No. The current implementation in lib/request-security.ts performs literal string comparison after normalization. The normalizeConfiguredHostname() function treats entries as exact hostnames or IP addresses, meaning wildcard patterns like *.example.com or CIDR notation like 10.0.0.0/24 are not expanded. You must list each specific hostname or IP literal individually.
Why does pi-web return 403 Forbidden for my proxy even when the hostname is configured?
Check for hostname normalization mismatches. The validation logic lower-cases hostnames and strips trailing dots, but mismatches can occur if your proxy sends a port number (e.g., host:8080) that isn't accounted for in the configuration, or if the Host header contains unexpected suffixes. Verify the exact header value using request logs, as isApiRequestHostAllowed() compares against the exact normalized form extracted by hostnameFromAuthority().
How does PI_WEB_ALLOWED_HOSTS interact with the proxy middleware in app/api/proxy.ts?
The proxy.ts middleware delegates to the security layer. When a request hits the API proxy endpoint, the code at lines 11-18 invokes isApiRequestAllowed(), which internally calls isApiRequestHostAllowed() to check the hostname against the PI_WEB_ALLOWED_HOSTS list. If this check fails, the middleware returns a 403 response immediately, preventing the request from reaching upstream handlers. This integration ensures that environment-based host restrictions are enforced at the edge of the application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →