# How PI_WEB_ALLOWED_HOSTS Configures Trusted Proxy Hostnames in pi-web

> Learn how PI_WEB_ALLOWED_HOSTS configures trusted proxy hostnames in pi-web by validating the HTTP Host header against a normalized allow-list for secure API requests.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-31

---

**`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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`:

```typescript
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:

```typescript
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`](https://github.com/agegr/pi-web/blob/main/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

# .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:

```yaml
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:

```typescript
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_HOSTNAME` in [`lib/request-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/request-security.ts).
- The **validation logic** resides in `configuredHostnamesFromEnvironment()` for parsing and `isApiRequestHostAllowed()` 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.ts`](https://github.com/agegr/pi-web/blob/main/app/api/proxy.ts) enforces 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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.