Resolve OQ-07: add multi-config listener support (ADR-019)
Introduce [[listeners]] configuration to support both dedicated-IP (1 IP = 1 cert = 1 domain) and shared-IP (SAN certificate) deployment models. Each listener is an independent TLS endpoint with its own bind address, TLS config, and site routing. OQ-07 is now resolved. Changes: - Add ADR-019 for multi-config listener support - Update config format from [server] to [[listeners]] entries - Update tls.md for per-listener TLS and certificate provisioning - Update overview.md architecture diagram and scope - Update proxy.md for per-listener HTTP redirect - Fix stale references in ADR-010, ADR-011, ADR-016 - Update OQ-05 resolution (per-listener bind_addr supersedes) - Add unique-host rationale to config validation rules - Architecture review: fix all 3 critical and 6 warning issues
This commit is contained in:
@@ -31,7 +31,8 @@ and `rustls-acme` supports multi-domain certificates natively.
|
||||
Move multi-site support from Phase 2 into Phase 1. The proxy supports multiple
|
||||
sites from the initial release:
|
||||
|
||||
- `[[sites]]` array in config (already the planned format)
|
||||
- `[[listeners.sites]]` array in each listener config (after ADR-019; was
|
||||
`[[sites]]` at top level)
|
||||
- Host-based routing via axum's `Host` extractor (already the planned approach)
|
||||
- Multi-domain ACME certificate provisioning via `rustls-acme`
|
||||
- Each site maps a hostname to an upstream address
|
||||
@@ -78,8 +79,7 @@ Phase 3 remains future enhancements.
|
||||
- Slightly more testing surface (must verify correct routing with multiple
|
||||
sites)
|
||||
- Must test multi-domain ACME provisioning (not just single-domain)
|
||||
- Wildcard or fallback site behavior needs to be defined (addressed in
|
||||
OQ-07)
|
||||
- Wildcard or fallback site behavior is defined by the listener's site routing
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -30,23 +30,22 @@ certificate covering all proxied domains. Manual mode uses certificate file
|
||||
paths (single cert file with all domains, or one cert per domain resolved via
|
||||
SNI).
|
||||
|
||||
The config format changes from the previous single-domain format:
|
||||
With ADR-019, TLS configuration lives inside `[[listeners]]` entries. Each
|
||||
listener has its own TLS mode and domain list. The config format is:
|
||||
|
||||
```toml
|
||||
# Previous (single-domain) format — no longer used
|
||||
[tls]
|
||||
mode = "acme"
|
||||
acme_domain = "git.alk.dev" # single string
|
||||
```
|
||||
# Current format (after ADR-019)
|
||||
[[listeners]]
|
||||
bind_addr = "203.0.113.10"
|
||||
|
||||
To the current multi-domain format:
|
||||
|
||||
```toml
|
||||
[tls]
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["git.alk.dev", "alk.dev"] # array of strings
|
||||
```
|
||||
|
||||
The previous single-listener format (pre-ADR-019) used a `[server.tls]` section
|
||||
which is no longer valid.
|
||||
|
||||
In ACME mode, `rustls-acme` provisions a single certificate covering all
|
||||
listed domains via Subject Alternative Names (SAN). This is the standard
|
||||
Let's Encrypt approach for multi-domain certificates.
|
||||
@@ -82,11 +81,12 @@ certificate or separate certificates resolved via SNI).
|
||||
domains must be validated) — mitigated by Let's Encrypt's domain-level
|
||||
validation
|
||||
- Per-site TLS configuration (e.g., a domain with a manual cert) requires a
|
||||
future config extension (OQ-07)
|
||||
future config extension — addressed by ADR-019 (multi-config listeners)
|
||||
|
||||
## References
|
||||
|
||||
- [tls.md](../tls.md)
|
||||
- [config.md](../config.md)
|
||||
- ADR-010 (multi-site in Phase 1)
|
||||
- ADR-004 (ACME-primary certificate management)
|
||||
- ADR-004 (ACME-primary certificate management)
|
||||
- ADR-019 (multi-config listener support)
|
||||
@@ -18,8 +18,9 @@ deployment.
|
||||
|
||||
## Decision
|
||||
|
||||
The `bind_addr` field must be an explicit IP address. `0.0.0.0` is rejected
|
||||
during config validation. The proxy will not start if `bind_addr` is `0.0.0.0`.
|
||||
The `bind_addr` field on each `[[listeners]]` entry must be an explicit IP
|
||||
address. `0.0.0.0` is rejected during config validation. The proxy will not
|
||||
start if any listener's `bind_addr` is `0.0.0.0`.
|
||||
|
||||
## Rationale
|
||||
|
||||
|
||||
171
docs/architecture/decisions/019-multi-config-listeners.md
Normal file
171
docs/architecture/decisions/019-multi-config-listeners.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# ADR-019: Multi-Config Listener Support
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
OQ-07 asked whether per-site TLS overrides should be supported for mixed
|
||||
ACME/manual domains. The original framing assumed a single listener with one
|
||||
TLS configuration, where the question was about mixing certificate sources
|
||||
within that listener.
|
||||
|
||||
However, there are two distinct deployment models for multi-domain TLS:
|
||||
|
||||
1. **Shared-IP multi-domain**: One IP address, one TLS listener, one SAN
|
||||
certificate covering multiple domains via SNI. All domains share the same
|
||||
ACME configuration. This is what the current architecture documents describe.
|
||||
|
||||
2. **Dedicated-IP single-domain (multi-config)**: Each IP address gets its own
|
||||
SSL certificate for its own domain. In this model, 1 IP = 1 SSL = 1 domain.
|
||||
Each listener is independently configured with its own bind address, TLS
|
||||
certificate, and site mapping. No SNI multiplexing is needed because each
|
||||
domain has its own IP.
|
||||
|
||||
The actual deployment uses model 2 (dedicated-IP single-domain). The proxy
|
||||
should support both models, and the choice between them should be a deployment
|
||||
concern, not an architectural limitation.
|
||||
|
||||
There are two approaches to supporting model 2:
|
||||
|
||||
- **Multiple instances**: Run separate `reverse-proxy` processes, each with
|
||||
their own config file binding to a different IP. Simple, isolated, no
|
||||
code changes needed.
|
||||
- **Multi-listener single process**: One process with a `[[listeners]]`
|
||||
config that defines multiple independent listeners, each with their own
|
||||
bind address, TLS config, and site mapping. More complex but shares
|
||||
resources (process overhead, connection pool, logging).
|
||||
|
||||
## Decision
|
||||
|
||||
Support both deployment models by extending the configuration format with
|
||||
`[[listeners]]`, where each listener is an independent TLS endpoint with its
|
||||
own bind address, TLS configuration, and site routing.
|
||||
|
||||
The config format changes from a single implicit listener to explicit
|
||||
`[[listeners]]` entries:
|
||||
|
||||
```toml
|
||||
# Multi-config: two listeners, each on a different IP
|
||||
[[listeners]]
|
||||
bind_addr = "203.0.113.10"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["git.alk.dev"]
|
||||
acme_cache_dir = "/var/lib/reverse-proxy/acme-cache"
|
||||
acme_directory = "production"
|
||||
|
||||
[[listeners.sites]]
|
||||
host = "git.alk.dev"
|
||||
upstream = "127.0.0.1:3000"
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "203.0.113.11"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "manual"
|
||||
cert_path = "/etc/ssl/alk.dev/fullchain.pem"
|
||||
key_path = "/etc/ssl/alk.dev/privkey.pem"
|
||||
|
||||
[[listeners.sites]]
|
||||
host = "alk.dev"
|
||||
upstream = "127.0.0.1:8080"
|
||||
```
|
||||
|
||||
Each `[[listeners]]` entry is an independent TLS endpoint with:
|
||||
- Its own `bind_addr` (required, no `0.0.0.0`; see ADR-016)
|
||||
- Its own `http_port` and `https_port`
|
||||
- Its own `tls` configuration (ACME or manual)
|
||||
- Its own `[[sites]]` array for host-based routing
|
||||
|
||||
The single-listener case is a natural subset of this format — a config with
|
||||
one `[[listeners]]` entry behaves identically to the current single-listener
|
||||
design. The multi-domain SAN case (one IP, one cert, multiple domains) is
|
||||
also supported: a single listener with multiple domains in `acme_domains`
|
||||
and multiple `[[sites]]`.
|
||||
|
||||
Global settings (logging, rate limiting, body limits, health check, admin
|
||||
socket) are top-level configuration. Listener-specific settings (bind address,
|
||||
ports, TLS, sites) live inside each `[[listeners]]` entry.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```toml
|
||||
# Global settings
|
||||
health_check_port = 9900
|
||||
admin_socket_path = "/run/reverse-proxy/admin.sock"
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
format = "text"
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
# Listener definitions
|
||||
[[listeners]]
|
||||
bind_addr = "203.0.113.10"
|
||||
# ... listener config ...
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
- The dedicated-IP model (1 IP = 1 SSL = 1 domain) is our actual deployment.
|
||||
The architecture should natively support it.
|
||||
- The shared-IP multi-domain model (SAN certificate) is also common and useful.
|
||||
Supporting both is straightforward with the `[[listeners]]` format.
|
||||
- Multiple instances would work but wastes resources (separate processes,
|
||||
separate connection pools, separate logging pipelines) for what is
|
||||
fundamentally the same binary doing the same job.
|
||||
- A single process with multiple listeners is more efficient: one process, one
|
||||
log pipeline, one config reload mechanism, one health check endpoint.
|
||||
- The `[[listeners]]` format is a natural extension — each listener is
|
||||
independent, but global concerns (logging, rate limiting, health checks)
|
||||
are shared, which is correct since they're all the same proxy.
|
||||
- The single-listener case is a degenerate case of `[[listeners]]` with one
|
||||
entry, so existing configurations translate trivially.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Natively supports both deployment models (dedicated-IP and shared-IP)
|
||||
- Single process for all listeners: shared logging, config reload, health
|
||||
checks, rate limiting
|
||||
- Each listener is fully independent — different IPs, different TLS configs,
|
||||
different domains
|
||||
- The config format is regular and predictable — `[[listeners]]` is a
|
||||
natural TOML array of tables
|
||||
- Mixed ACME/manual configurations are naturally supported: each listener
|
||||
chooses its own TLS mode
|
||||
- The single-listener, single-domain case is trivially supported
|
||||
|
||||
**Negative:**
|
||||
- Config format change from `[server]` with implicit single listener to
|
||||
`[[listeners]]` with explicit listener entries
|
||||
- The proxy must manage multiple TCP listeners and TLS acceptors, which
|
||||
adds implementation complexity
|
||||
- Each listener needs its own ACME state machine (in ACME mode), increasing
|
||||
resource usage proportional to listener count
|
||||
- Global rate limiting applies across all listeners (per-IP rate limiting
|
||||
doesn't distinguish which listener received the request — this is correct
|
||||
behavior since the IP is the same regardless of listener)
|
||||
|
||||
## References
|
||||
|
||||
- [config.md](../config.md)
|
||||
- [tls.md](../tls.md)
|
||||
- [overview.md](../overview.md)
|
||||
- ADR-008 (static/dynamic config split)
|
||||
- ADR-011 (multi-domain TLS config)
|
||||
- ADR-016 (explicit bind address)
|
||||
- OQ-05 (multiple bind addresses — now resolved via per-listener `bind_addr`)
|
||||
Reference in New Issue
Block a user