headroomlabs-ai/headroom · error · ValueError

Invalid {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV} entry: {exc}

Error message

Invalid {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV} entry: {exc}

What it means

load_trusted_dashboard_client_cidrs() parses HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS (a comma-separated CIDR list identifying gateways whose X-Forwarded-For may be trusted) via ipaddress.ip_network(). Any entry the ip module rejects — bad mask, host bits set without strict=False, malformed address — is re-raised as this ValueError with the underlying ipaddress message chained. The check runs at request-handling/config load time, so a bad list can break trust resolution.

Source

Thrown at headroom/proxy/forwarded_headers.py:130

    the process environment. A malformed entry raises
    :class:`ValueError` — let it propagate so the failure is loud at
    startup instead of silently disabling the gate.
    """
    if raw is None:
        raw = os.environ.get(TRUSTED_GATEWAY_CIDRS_ENV, "")
    return _parse_cidr_list(raw)


def load_trusted_dashboard_client_cidrs(
    raw: str | None = None,
) -> tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...]:
    """Parse the Dashboard client CIDR allow-list from its environment variable."""
    if raw is None:
        raw = os.environ.get(TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV, "")
    try:
        return _parse_cidr_list(raw)
    except ValueError as exc:
        raise ValueError(f"Invalid {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV} entry: {exc}") from exc


def _normalize_ip(
    host: str,
) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
    """Parse ``host`` into an IPv4/IPv6 address, unmapping ``::ffff:*``.

    IPv4-mapped IPv6 addresses (``::ffff:10.0.0.1``) — emitted by Linux
    dual-stack sockets — are normalized to their underlying IPv4 form
    so a CIDR allow-list of ``10.0.0.0/8`` matches them naturally.
    Returns ``None`` on malformed input; callers treat that as "not a
    trusted gateway".
    """
    return normalize_ip(host)


def _peer_host(request: Any) -> str | None:
    """Pull ``request.client.host`` defensively (TestClient may omit)."""

View on GitHub (pinned to 322425c43b)

Solutions

  1. Use well-formed CIDR networks: 10.0.0.0/8, 192.168.0.0/16, 2001:db8::/32; write single hosts as a.b.c.d/32.
  2. Validate the list before deploying: python -c "import ipaddress,sys; [ipaddress.ip_network(s.strip()) for s in sys.argv[1].split(',') if s.strip()]" "$VALUE".
  3. Remove trailing commas and stray empty entries from comma-separated lists.

Example fix

# before
export HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS="10.0.0.1/8"

# after
export HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS="10.0.0.0/8"
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress

def cidr_list_ok(raw: str) -> str | None:
    try:
        nets = [ipaddress.ip_network(s.strip()) for s in raw.split(",") if s.strip()]
        return None
    except ValueError as exc:
        return str(exc)

Try / catch

try:
    cidrs = load_trusted_dashboard_client_cidrs(raw)
except ValueError as exc:
    raise ConfigError(f"fix {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV}: {exc}") from exc

Prevention

When it happens

Trigger: Setting HEADROOM_PROXY_TRUSTED_DASHBOARD_CLIENT_CIDRS to a value containing an invalid CIDR: '10.0.0.1/8' (host bits set — strict networks required), '10.0.0.0/33' (bad mask), 'not-an-ip/8', '10.0.0.0/8,,' (empty segment from a trailing comma may or may not be filtered — invalid segments are rejected).

Common situations: Entering a single host IP where a network is expected (10.0.0.1 instead of 10.0.0.1/32); IPv6 zone identifiers or compressed forms that ipaddress rejects; copy-pasting a list with a trailing comma or whitespace-only entry; typos in the mask.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/b49c55ba36d3fdba. Report an issue: GitHub.