Files
dashcaddy/dashcaddy-installer/templates/Caddyfile.template
Hermes 933606ce3f fix(caddy-admin): IPv6 loopback origin allowlist + bracket-strip helper (DC-069) [glm-grade=A]
Two coupled bugs that, together, cause the live 'admin.api received request
from ::1 → 403 client is not allowed to access from origin' noise on DNS2:

  (1) Caddyfile 'origins' allowlist (admin 0.0.0.0:2019 block on DNS2) had
      4 IPv4 entries (localhost/127.0.0.1/172.17.0.1/0.0.0.0) but no IPv6
      entry. Per glibc RFC 3484 + /etc/hosts '::1 localhost', Node's
      dns.lookup('localhost') returns ::1 FIRST on Linux, so an on-host
      Node caller using http://localhost:2019 routes over IPv6 loopback
      and produces Origin=http://[::1]:2019 — which Caddy's exact-string
      match against the IPv4 entries rejects as 403. Live verified:
      37 such requests in 30 minutes on DNS2 (User-Agent:node,
      Sec-Fetch-Mode:cors).

  (2) _httpFetch (src/utils/http.js) was broken for IPv6 literal URLs:
      on Node 22, new URL('http://[::1]:2019/x').hostname === '[::1]'
      (brackets preserved), but http.request({hostname}) needs the
      BRACKETLESS form for actual TCP connect. Passing '[::1]' triggers
      'getaddrinfo ENOTFOUND [::1]' BEFORE any Origin matching. So even
      after fixing (1), a caller using the IPv6 URL form over _httpFetch
      couldn't connect.

Fixes:

  (1) _httpFetch computes transportHostname by stripping leading [ and
      trailing ] when parsed.hostname is bracket-wrapped. transports via
      bracketless form. defaultOrigin keeps bracket form so Caddy's
      allowlist exact-matches. Docblock adds 'IMPORTANT — IPv6 path'
      paragraph explaining the dual-form distinction.

  (2) dashcaddy-installer/templates/Caddyfile.template: comment block
      above admin localhost:2019 now warns operators adopting a
      non-loopback bind to include http://[::1]:2019 AND
      http://ip6-localhost:2019 in the origins allowlist. Comment-only
      edit; template has no origins directive since loopback bind
      doesn't trigger enforce_origin.

Tests (NEW utils-http-caddy-admin-ipv6-origin.test.js, 4 cases):
  - template comment mentions IPv6 ([::1]/ip6-localhost/IPv6 substring)
  - stripComments helper preserves template literals with // inside
    (eslint no-control-regex forces non-regex split)
  - end-to-end: real http server on [::1]:20191, fetchT succeeds 200,
    Origin header is exactly 'http://[::1]:20191'
  - end-to-end bug repro: same setup with IPv4-only allowlist returns
    403 (proves the mock allowlist check actually runs)

DC-051's utils-http-caddy-admin-origin.test.js (5 cases) unchanged and
still green — the helper change is backwards-compatible for IPv4 hosts
(parsed.hostname.startsWith('[') is false for 127.0.0.1/localhost/
172.17.0.1).

Full suite: 2281/2281 (98 suites, +4 net new). ESLint clean on touched
files.

GLM-5.3 judge round 1 (35s, 3 tool calls): GRADE=A. 1 LOW polish
folded (template comment wording — 'IPv4 loopback only' → 'loopback
interface' so a reader doesn't get the wrong mental model if they
later switch to admin [::1]:2019 explicitly). No blocking issues.
2026-08-18 13:34:51 -07:00

39 lines
1.3 KiB
Caddyfile

# DashCaddy Caddyfile
# Generated by DashCaddy Installer
# Global options
{
# The default `admin localhost:2019` binds to the loopback interface, so
# Caddy's `enforce_origin` CSRF guard is never engaged and no `origins`
# directive is required. (Note: glibc resolves `localhost` to `::1`
# first per RFC 3484, so `admin localhost:2019` typically binds BOTH
# IPv4 and IPv6 loopback — the actionable point is that any loopback
# bind skips enforce_origin, not the exact IPv4/IPv6 split.)
#
# If a non-loopback bind is adopted later (e.g. `admin 0.0.0.0:2019 { ... }`
# so a docker container on the host's bridge can reach admin via
# 172.17.0.1:2019), the admin block MUST include an `origins` allowlist.
# On Linux, `localhost` resolves to `::1` FIRST per glibc RFC 3484 (because
# /etc/hosts has `::1 localhost`), so allowlist entries must include the
# IPv6 literal form `http://[::1]:2019` AND `http://ip6-localhost:2019`
# (the glibc alias) — `http://localhost:2019` alone will 403 every probe
# that resolves localhost to `::1`. See DC-051 + DC-069 in repo history.
admin localhost:2019
auto_https off
}
# Dashboard
:{{PORT}} {
root * {{DASHBOARD_PATH}}
file_server
encode gzip
# API proxy
handle /api/* {
reverse_proxy localhost:{{API_PORT}}
}
# SPA fallback
try_files {path} /index.html
}