# How celld Handles Forward Trusted Headers in Production Deployments

> Learn how celld handles forward trusted headers in production. Discover how to enable trusted forwarding and prevent client spoofing for secure deployments.

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: how-to-guide
- Published: 2026-08-15

---

**By default, celld ignores `X-Forwarded-Host` and `X-Forwarded-Proto` headers to prevent client spoofing, but operators can enable trusted forwarding via `--trust-forwarded-headers` or `CELLD_TRUST_FORWARDED_HEADERS=1` to extract the last comma-separated value from each header.**

The celld runtime, developed by Deno, takes a security-first approach to handling **forward trusted headers**. In production deployments behind reverse proxies or load balancers, applications typically need the original client-facing URL scheme and host. Rather than blindly trusting potentially malicious client headers, celld requires explicit opt-in and implements a strict parsing strategy that assumes upstream proxies replace rather than append to these headers.

## Default Behavior: Headers Are Untrusted

Out of the box, celld constructs the effective request URL solely from the HTTP request line. The `X-Forwarded-Host` and `X-Forwarded-Proto` headers are **completely ignored**, preventing clients from manipulating how the application perceives its own URL.

This default is implemented in [`crates/celld/main.rs`](https://github.com/denoland/celld/blob/main/crates/celld/main.rs) inside the `request_url` function. When `trust_forwarded_headers` is `false`, the function falls back to the standard `Host` header and hardcoded `http` scheme:

```rust
let host = header("host", false).unwrap_or("celld.local");
let scheme = "http";

```

This ensures that even if a malicious actor sends `X-Forwarded-Proto: https` to force secure cookie behavior or bypass security checks, celld remains unaffected.

## Enabling Trusted Forwarded Headers

Operators activate trusted header processing through one of two mechanisms defined in [`crates/celld/main/cli.rs`](https://github.com/denoland/celld/blob/main/crates/celld/main/cli.rs):

1. **Command-line flag**: `--trust-forwarded-headers`
2. **Environment variable**: `CELLD_TRUST_FORWARDED_HEADERS=1`

The CLI parsing code handles both sources between lines 23 and 70, storing the result in the `Settings` struct. This propagates through `AppHandle.trust_forwarded_headers` to the HTTP handler.

When enabled, celld changes its header parsing strategy significantly.

## How celld Parses Forwarded Headers When Trusted

The critical logic resides in [`crates/celld/main.rs`](https://github.com/denoland/celld/blob/main/crates/celld/main.rs) at lines 3129–3146. The `request_url` function uses a closure-based approach:

```rust
fn request_url(parts: &hyper::http::request::Parts, trust_forwarded_headers: bool) -> String {
    let header = |name: &str, take_last: bool| { 
        // Returns Option<String> by parsing header values
        // When take_last is true, splits on comma and returns final element
        ...
    };
    
    let forwarded = |name: &str| {
        trust_forwarded_headers.then(|| header(name, true)).flatten()
    };
    
    let host = forwarded("x-forwarded-host")
        .or_else(|| header("host", false))
        .unwrap_or("celld.local");
        
    let scheme = forwarded("x-forwarded-proto").unwrap_or("http");
    
    // path_and_query comes directly from request-target, never from headers
    format!("{scheme}://{host}{path_and_query}")
}

```

Key implementation details:

- **`take_last: true`** — When trusting headers, celld extracts the **final** comma-separated value from multi-valued headers. This assumes a trusted proxy has **replaced** the entire header value, eliminating any client-supplied prefix.
- **Fallback chain** — If forwarded headers are absent, celld degrades gracefully to `Host` header and `http` scheme.
- **Path protection** — The request-target path and query are always taken directly from the request line, never from headers, preventing additional attack vectors.

## Security Requirements for Production

The [`docs/security.md`](https://github.com/denoland/celld/blob/main/docs/security.md) file (lines 38–46) explicitly documents the deployment prerequisites for enabling this flag. Operators must ensure:

- A **trusted proxy** sits in front of celld
- The proxy **replaces** both `X-Forwarded-Host` and `X-Forwarded-Proto` entirely
- The proxy does not merely **append** to existing values, which would leave client-controlled data in earlier comma-separated positions

This design intentionally breaks the de-facto standard of reading the *first* forwarded value. By reading the *last*, celld forces infrastructure operators to configure proper header sanitization at the edge.

## Practical Deployment Examples

### Basic Enablement

```bash

# CLI flag approach

celld --bucket s3://my-fleet-bucket --trust-forwarded-headers

```

```bash

# Environment variable approach

export CELLD_TRUST_FORWARDED_HEADERS=1
celld --bucket s3://my-fleet-bucket

```

### Impact on Application Code

When enabled, Durable Object handlers receive the reconstructed public URL:

```rust
// With --trust-forwarded-headers
let url = request.url(); // "https://api.example.com/v1/data"

```

Without the flag, the same code produces:

```rust
// Default behavior
let url = request.url(); // "http://celld.local/v1/data"

```

### Replicating celld's Logic

Applications needing to implement identical behavior can reference the internal structure:

```rust
// Mirrors celld's request_url implementation
fn effective_url(req: &hyper::Request<hyper::body::Body>, trust: bool) -> String {
    celld::runtime::request_url(req.parts(), trust)
}

```

## Related Configuration Files

| File | Purpose | Key Lines |
|------|---------|-----------|
| [`crates/celld/main/cli.rs`](https://github.com/denoland/celld/blob/main/crates/celld/main/cli.rs) | Parses CLI flags and env vars | 23–70 |
| [`crates/celld/main.rs`](https://github.com/denoland/celld/blob/main/crates/celld/main.rs) | Implements `request_url` header logic | 3129–3146 |
| [`docs/security.md`](https://github.com/denoland/celld/blob/main/docs/security.md) | Documents forwarded header policy | 38–46 |
| [`crates/celld/runtime.rs`](https://github.com/denoland/celld/blob/main/crates/celld/runtime.rs) | Invokes URL construction per request | Runtime integration |

## Summary

- **Default security**: celld completely ignores `X-Forwarded-*` headers, using only the request line and `Host` header
- **Opt-in activation**: `--trust-forwarded-headers` or `CELLD_TRUST_FORWARDED_HEADERS=1` required in production
- **Last-value parsing**: Enabled mode extracts the final comma-separated element, assuming proxy replacement
- **Infrastructure dependency**: Requires trusted upstream proxy that fully replaces both headers, never appends
- **Graceful degradation**: Missing forwarded headers fall back to standard behavior without failure

## Frequently Asked Questions

### What happens if I enable `--trust-forwarded-headers` without a proxy?

Your application becomes vulnerable to **host header injection attacks**. Clients can set arbitrary `X-Forwarded-Host` and `X-Forwarded-Proto` values, causing authentication callbacks, cookie domains, and URL generation to use attacker-controlled values. Only enable this when a trusted proxy sanitizes these headers.

### Why does celld use the last value instead of the first?

The first value in a comma-separated forwarded header typically comes from the original client. Subsequent values are added by each proxy hop. By reading the **last** value, celld ensures that only the final trusted proxy's replacement value is honored—assuming the proxy was configured to replace rather than append. This inverts the typical "first trusted" pattern to enforce stricter infrastructure requirements.

### Does celld support `Forwarded` header (RFC 7239)?

No. As implemented in denoland/celld, only `X-Forwarded-Host` and `X-Forwarded-Proto` are parsed. The standardized `Forwarded` header is not processed even when trust mode is enabled. Applications requiring RFC 7239 support must parse that header manually.

### Can I trust only one header (host or proto, but not both)?

No. The `trust_forwarded_headers` flag is binary—both `X-Forwarded-Host` and `X-Forwarded-Proto` are either trusted together or ignored together. This prevents partial trust configurations that could create scheme/host mismatches in URL construction.