Skip to content

ingress: allow per-port listen-address override (tailnet-only ingresses)#271

Draft
rgarcia wants to merge 1 commit into
mainfrom
ingress-per-port-listen-address
Draft

ingress: allow per-port listen-address override (tailnet-only ingresses)#271
rgarcia wants to merge 1 commit into
mainfrom
ingress-per-port-listen-address

Conversation

@rgarcia
Copy link
Copy Markdown
Contributor

@rgarcia rgarcia commented Jun 5, 2026

Use case

Hypeman's ingress (Caddy) currently binds every listen port to a single
global address (caddy.listen_address, default 0.0.0.0), so all ingress
ports are exposed on the public NIC.

We want the CDP (9222) and ChromeDriver (9224) ingresses to be reachable
only on a non-public interface (the host's Tailscale IP) so they are never
served on the public NIC — defense-in-depth at the listener, complementing the
host firewall (which is also being restricted for 9222/9224). The browser API
(444) and VNC (443) ingresses must stay public.

Change

Add an optional config map under caddy that overrides the listen address for
specific listen ports. Ports not present in the map fall back to the global
listen_address.

caddy:
  listen_address: 0.0.0.0        # global default, unchanged
  port_listen_addresses:
    9222: "100.107.186.40"       # CDP        -> tailnet only
    9224: "100.107.186.40"       # ChromeDriver -> tailnet only

Caddy already supports a per-server listen array, so this is a natural
extension: buildConfig builds one server per listen port, and now resolves
the bind address per port via listenAddressForPort(port) instead of always
using the global address.

The field is threaded through:
CaddyConfig.PortListenAddresses (cmd/api/config) → ingress.Config.PortListenAddresses
CaddyConfigGenerator → per-server listen in buildConfig.

This is config-level (not a per-Ingress API field) so it fits the
Ansible-managed host config and the e2e API client does not need to know the
Tailscale IP.

Backward compatibility

  • caddy.listen_address default stays 0.0.0.0.
  • When port_listen_addresses is unset or empty, every server binds the global
    listen address exactly as before — no behavior change.
  • An empty string value for a port also falls back to the global address.

Tests

  • lib/ingress table-driven generator tests: override applied (CDP/ChromeDriver
    on the tailscale IP, 443/444 still on 0.0.0.0), nil map, empty map, and
    empty-value fall-through.
  • cmd/api/config tests: YAML loads the port_listen_addresses map while
    listen_address stays 0.0.0.0; default config has no overrides.
  • go build ./..., go vet ./..., and go test ./lib/ingress/... ./cmd/api/config/... all pass locally.

🤖 Generated with Claude Code

Add an optional caddy.port_listen_addresses map (listen port -> bind
address) that overrides the global caddy.listen_address for specific
Caddy servers. Ports not present in the map continue to use the global
listen address.

Use case: bind the CDP (9222) and ChromeDriver (9224) ingresses to a
non-public interface (e.g. the host's Tailscale IP) so they are not
reachable on the public NIC, complementing the host firewall, while the
browser API (444) and VNC (443) ingresses stay public on 0.0.0.0.

The config field is threaded from CaddyConfig through ingress.Config and
into CaddyConfigGenerator, which now resolves the bind address per listen
port via listenAddressForPort(). Default behavior is unchanged: when the
map is unset/empty, every server binds the global listen_address
(default 0.0.0.0).

Adds table-driven tests for the generator (override applied, unset, empty
map, and empty override value fall-through) and config-load tests for the
YAML mapping and the empty default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant