Add container deployment model (ADR-020) and fix review issues
- ADR-020: Document defense-in-depth rationale for running in a minimal Docker container (memory-safe language + container isolation), flexible upstream addressing (Docker DNS, loopback, LAN, tunnel endpoints), file-primary logging for fail2ban, and volume mount strategy - ADR-016: Add allow_wildcard_bind override for container deployments where 0.0.0.0 is correct inside the container network namespace - operations.md: Add container deployment section with Docker Compose example, networking table, volume mounts, and health check integration; flip logging to file-primary for fail2ban reliability; note systemd as alternative to container deployment - config.md: Restructure logging fields into nested LoggingConfig (matching TOML [logging] section), add allow_wildcard_bind, shutdown_timeout_secs, and log_file_path fields; clarify upstream addressing supports Docker DNS and tunnel endpoints; update validation rule for 0.0.0.0 override - overview.md: Update architecture diagram for container model with Docker networking and volume mounts; add ADR-020 reference - proxy.md: Clarify X-Forwarded-Proto is determined by listener port, not hardcoded 80/443 - ADR-013: Fix health_check_port default contradiction (default is 9900, not 0/disabled as previously stated)
This commit is contained in:
@@ -29,9 +29,10 @@ Add a configurable health check port that binds to `127.0.0.1` only (localhost),
|
||||
serving `/health` over plain HTTP. This is a separate listener from the main
|
||||
HTTP and HTTPS listeners.
|
||||
|
||||
The port is configurable via `health_check_port` in StaticConfig. Setting it
|
||||
to `0` (default) disables the separate health check listener, and `/health`
|
||||
remains available on the main HTTPS listener as a fallback.
|
||||
The port is configurable via `health_check_port` in StaticConfig. The default
|
||||
value is `9900` (enabled, localhost only). Setting it to `0` disables the
|
||||
separate health check listener, and `/health` remains available on the main
|
||||
HTTPS listener as a fallback.
|
||||
|
||||
## Rationale
|
||||
|
||||
|
||||
@@ -19,8 +19,15 @@ deployment.
|
||||
## Decision
|
||||
|
||||
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`.
|
||||
address. `0.0.0.0` is rejected during config validation unless explicitly
|
||||
overridden. The proxy will not start if any listener's `bind_addr` is
|
||||
`0.0.0.0` and the override is not enabled.
|
||||
|
||||
**Override mechanism**: `allow_wildcard_bind = true` in the config file, or
|
||||
`--allow-wildcard-bind` on the command line. This flag permits `0.0.0.0` as a
|
||||
valid bind address. It is intended for container deployments where the
|
||||
container's network namespace is isolated and `0.0.0.0` is the correct address
|
||||
(Docker publishes only the explicitly mapped ports).
|
||||
|
||||
## Rationale
|
||||
|
||||
@@ -31,24 +38,31 @@ start if any listener's `bind_addr` is `0.0.0.0`.
|
||||
- `0.0.0.0` is often a default in example configurations and can be deployed
|
||||
without the operator realizing the service is accessible on all interfaces
|
||||
- Rejecting it at validation time prevents this common mistake
|
||||
- If a deployment genuinely needs to bind all interfaces, `0.0.0.0` can be
|
||||
overridden with an explicit flag, but this should be a deliberate choice
|
||||
- In a container, `0.0.0.0` is correct and safe because the container's
|
||||
network namespace isolates it — only Docker-published ports are reachable
|
||||
from outside the container
|
||||
- The override is opt-in so it must be a deliberate choice, not an accident
|
||||
- This matches the principle of explicit over implicit for security-sensitive
|
||||
configuration
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Prevents accidental exposure on unintended network interfaces
|
||||
- Prevents accidental exposure on unintended network interfaces in bare-metal
|
||||
deployments
|
||||
- Forces operators to be deliberate about which interface the proxy serves
|
||||
- Config validation catches the mistake before deployment
|
||||
- Container deployments work correctly with `0.0.0.0` when the override is
|
||||
explicitly enabled
|
||||
|
||||
**Negative:**
|
||||
- Not suitable for deployments that genuinely need to bind all interfaces
|
||||
(mitigated by explicit override if needed in the future)
|
||||
- Slightly more configuration required (operator must know their public IP)
|
||||
- Container deployments require one extra config flag (`allow_wildcard_bind =
|
||||
true`) — a minor operational step
|
||||
- Slightly more configuration required for bare-metal (operator must know
|
||||
their public IP)
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-020: Container Deployment Model](020-container-deployment.md)
|
||||
- [config.md](../config.md)
|
||||
- [overview.md](../overview.md)
|
||||
97
docs/architecture/decisions/020-container-deployment.md
Normal file
97
docs/architecture/decisions/020-container-deployment.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# ADR-020: Container Deployment Model
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The reverse proxy replaces an nginx instance running directly on the host, which
|
||||
is vulnerable to multiple actively-exploited CVEs (including CVE-2026-42945,
|
||||
unauthenticated RCE). Rust's memory safety eliminates the bug class responsible
|
||||
for most nginx CVEs, but containerization adds a second independent barrier.
|
||||
|
||||
Running the proxy in a minimal Docker container means that even if an attacker
|
||||
finds a logic-level vulnerability in the proxy, they must also escape the
|
||||
container boundary. The probability of chaining a Rust proxy exploit with a
|
||||
container escape is materially lower than exploiting any single layer alone.
|
||||
|
||||
Additionally, the proxy must support flexible upstream addressing. While the
|
||||
initial deployment runs all services on the same host (Gitea, Postgres, the
|
||||
proxy itself in containers sharing a Docker network), upstreams may also be on
|
||||
different machines — reachable via LAN IP, VPN, or SSH-based tunnel (alknet).
|
||||
The configuration must not assume same-host deployment.
|
||||
|
||||
Finally, fail2ban integration requires reliable log access. Docker log drivers
|
||||
(journald, json-file) introduce complexity and fragility for log parsing. A
|
||||
log file on a volume mount is simpler, more reliable, and easier for fail2ban
|
||||
to consume directly from the host filesystem.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **The proxy runs in a minimal Docker container** (multi-stage build:
|
||||
compile in `rust:alpine`, run in `alpine` or `scratch`). The container
|
||||
image contains only the static binary and necessary runtime files.
|
||||
|
||||
2. **Bind address `0.0.0.0` is allowed inside containers** via an explicit
|
||||
opt-in flag (`allow_wildcard_bind = true` in config, or
|
||||
`--allow-wildcard-bind` CLI flag). The default remains: reject `0.0.0.0`
|
||||
as a bind address. In a container, binding `0.0.0.0` is correct and safe
|
||||
because the container's network namespace is isolated — it only exposes
|
||||
interfaces Docker publishes via `-p` flags. See ADR-016 for the original
|
||||
rationale and this override.
|
||||
|
||||
3. **Upstream addressing is unopinionated**. The `upstream` field in
|
||||
`SiteConfig` accepts any `host:port` combination:
|
||||
- Docker DNS names (`gitea:3000`) for containers on the same Docker network
|
||||
- Loopback addresses (`127.0.0.1:3000`) for host-network or sidecar
|
||||
deployments
|
||||
- LAN IPs (`10.0.0.5:3000`) for same-network services
|
||||
- VPN or tunnel endpoints as needed
|
||||
The proxy makes no assumptions about upstream locality.
|
||||
|
||||
4. **Logging is file-primary for fail2ban integration.** The proxy writes
|
||||
structured logs to a file (default: `/var/log/reverse-proxy/access.log`)
|
||||
on a volume mount. stdout/stderr logging is always-on (for `docker logs`
|
||||
and `journalctl`). File logging is the authoritative source for fail2ban
|
||||
because it avoids the fragility of Docker log driver parsing.
|
||||
|
||||
5. **ACME state and admin socket are volume-mounted.** The ACME cache directory
|
||||
(`/var/lib/reverse-proxy/acme-cache/`) and admin socket
|
||||
(`/run/reverse-proxy/admin.sock`) are mounted as volumes so state persists
|
||||
across container restarts and the host can send reload commands.
|
||||
|
||||
6. **Health checks use Docker's native mechanism.** The health check endpoint
|
||||
on port 9900 (localhost only) is used directly by Docker's `HEALTHCHECK`
|
||||
directive. No port publishing needed.
|
||||
|
||||
7. **SSH passthrough for Gitea** remains on the host. The proxy does not handle
|
||||
SSH traffic. Port 22 traffic is routed directly to the Gitea container by
|
||||
Docker, not through the reverse proxy. This matches the current nginx
|
||||
deployment where SSH is independent.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Defense-in-depth: memory-safe language + container isolation + minimal image
|
||||
- Flexible upstream addressing supports same-host, LAN, VPN, and tunnel
|
||||
deployments without config changes
|
||||
- File-based logging is simple and reliable for fail2ban — no Docker log
|
||||
driver parsing required
|
||||
- ACME state persists across container restarts via volume mounts
|
||||
- Health check integrates natively with Docker's orchestration
|
||||
|
||||
**Negative:**
|
||||
- Slightly more complex deployment (Docker Compose, volume mounts, network
|
||||
configuration) compared to a bare binary
|
||||
- `0.0.0.0` override must be documented clearly to prevent misuse in
|
||||
non-container deployments
|
||||
- File logging requires a volume mount, adding a deployment step
|
||||
- Container rebuilds are needed for binary updates (mitigated by CI/CD)
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-016: Explicit Bind Address Requirement](016-explicit-bind-address.md)
|
||||
- [overview.md](../overview.md)
|
||||
- [operations.md](../operations.md)
|
||||
- [config.md](../config.md)
|
||||
Reference in New Issue
Block a user