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:
@@ -51,6 +51,7 @@ certificate via ACME.
|
||||
| [017](decisions/017-upstream-connection-defaults.md) | Upstream Connection Defaults | Accepted |
|
||||
| [018](decisions/018-body-size-limit.md) | Request Body Size Limit | Accepted |
|
||||
| [019](decisions/019-multi-config-listeners.md) | Multi-Config Listener Support | Accepted |
|
||||
| [020](decisions/020-container-deployment.md) | Container Deployment Model | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -86,10 +86,19 @@ Immutable after startup. Changes require a process restart.
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `listeners` | `Vec<ListenerConfig>` | Independent TLS endpoints, each with its own bind address and TLS config (see ADR-019) |
|
||||
| `allow_wildcard_bind` | `bool` | Allow `0.0.0.0` as a bind address. Required for container deployments. Default: `false` (see ADR-016, ADR-020) |
|
||||
| `health_check_port` | `u16` | Port for local health check endpoint (default: `9900`; set to `0` to disable; see ADR-013) |
|
||||
| `admin_socket_path` | `String` | Unix domain socket path for admin API (default: `/run/reverse-proxy/admin.sock`; empty string to disable; see ADR-014) |
|
||||
| `log_level` | `"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"` | Logging verbosity |
|
||||
| `log_format` | `"text"` or `"json"` | Log output format |
|
||||
| `shutdown_timeout_secs` | `u64` | Maximum seconds to wait for in-flight requests during graceful shutdown (default: `30`) |
|
||||
| `logging` | `LoggingConfig` | Logging configuration (see below) |
|
||||
|
||||
**LoggingConfig** (nested in `[logging]` TOML section):
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `level` | `"trace"`, `"debug"`, `"info"`, `"warn"`, `"error"` | Logging verbosity |
|
||||
| `format` | `"text"` or `"json"` | Log output format |
|
||||
| `log_file_path` | `String` | Path to log file. When set, structured logs are written to this file in addition to stdout/stderr. Strongly recommended for fail2ban integration in container deployments (see ADR-020). Default: not set (file logging disabled) |
|
||||
|
||||
**ListenerConfig** (per-listener static config):
|
||||
|
||||
@@ -127,7 +136,7 @@ connections immediately.
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `host` | `String` | Hostname to match (e.g., `"git.alk.dev"`) |
|
||||
| `upstream` | `String` | Upstream address (e.g., `"127.0.0.1:3000"`) |
|
||||
| `upstream` | `String` | Upstream address. Supports Docker DNS (`gitea:3000`), loopback (`127.0.0.1:3000`), LAN IPs, and tunnel endpoints. No assumption about upstream locality (see ADR-020) |
|
||||
| `upstream_scheme` | `"http"` or `"https"` | Protocol for upstream connection (default: `"http"`) |
|
||||
| `upstream_connect_timeout_secs` | `u64` | TCP connect timeout in seconds (default: `5`; see ADR-015, ADR-017) |
|
||||
| `upstream_request_timeout_secs` | `u64` | Full request timeout in seconds (default: `60`; see ADR-015, ADR-017) |
|
||||
@@ -195,6 +204,7 @@ admin_socket_path = "/run/reverse-proxy/admin.sock" # Empty string to disable
|
||||
[logging]
|
||||
level = "info"
|
||||
format = "text" # "text" or "json"
|
||||
# log_file_path = "/var/log/reverse-proxy/access.log" # Optional; always-on when set
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
@@ -251,6 +261,7 @@ admin_socket_path = "/run/reverse-proxy/admin.sock"
|
||||
[logging]
|
||||
level = "info"
|
||||
format = "text"
|
||||
# log_file_path = "/var/log/reverse-proxy/access.log" # Optional; always-on when set
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
@@ -285,7 +296,7 @@ upstream = "127.0.0.1:8080"
|
||||
On startup, the config is validated:
|
||||
|
||||
1. At least one `[[listeners]]` entry must exist
|
||||
2. Each listener's `bind_addr` is not `0.0.0.0` (must be explicit; see ADR-016)
|
||||
2. Each listener's `bind_addr` is not `0.0.0.0` unless `allow_wildcard_bind = true` (in config) or `--allow-wildcard-bind` (CLI flag) is set (see ADR-016, ADR-020)
|
||||
3. Each listener's `bind_addr` and `https_port` combination must be unique
|
||||
4. In ACME mode, `acme_domains` must be non-empty
|
||||
5. In manual mode, `cert_path` and `key_path` must both be set and the files
|
||||
@@ -321,6 +332,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
| [015](decisions/015-per-site-timeouts.md) | Per-site upstream timeouts with defaults | 5s connect / 60s request defaults, per-site overrides |
|
||||
| [016](decisions/016-explicit-bind-address.md) | Explicit bind address required | Rejects `0.0.0.0` to prevent accidental exposure |
|
||||
| [019](decisions/019-multi-config-listeners.md) | Multi-config listeners | `[[listeners]]` supporting both dedicated-IP and shared-IP deployment models |
|
||||
| [020](decisions/020-container-deployment.md) | Container deployment model | Flexible upstream addressing; `allow_wildcard_bind` override for containers |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -87,14 +87,35 @@ REQUEST client_ip=203.0.113.50 host=git.alk.dev method=GET path=/user/repo statu
|
||||
|
||||
### Output
|
||||
|
||||
Logs are written to:
|
||||
- **stdout/stderr**: For systemd/journald integration
|
||||
- **File** (optional): For fail2ban consumption at
|
||||
`/var/log/reverse-proxy/access.log`
|
||||
Logs are written to two destinations simultaneously:
|
||||
- **File** (primary): `/var/log/reverse-proxy/access.log` — the authoritative
|
||||
source for fail2ban consumption. File logging is always enabled when the
|
||||
`log_file_path` config is set. See ADR-020 for the rationale behind
|
||||
file-primary logging.
|
||||
- **stdout/stderr**: Always-on, for `docker logs`, `journalctl`, and
|
||||
development use. Structured in the same format as the file output.
|
||||
|
||||
The `tracing-subscriber` layer configuration supports both simultaneously via
|
||||
`Layer` composition.
|
||||
|
||||
### File Logging and fail2ban
|
||||
|
||||
File logging is the primary integration point for fail2ban. A log file on a
|
||||
volume mount is simpler and more reliable than parsing Docker log drivers or
|
||||
journald — no log driver configuration, no format conversion, no risk of
|
||||
dropping events.
|
||||
|
||||
In container deployments, the log directory is volume-mounted so fail2ban on
|
||||
the host can read it directly:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /var/log/reverse-proxy:/var/log/reverse-proxy
|
||||
```
|
||||
|
||||
A corresponding fail2ban filter definition and jail configuration are provided
|
||||
as part of the deployment documentation.
|
||||
|
||||
### Log Levels
|
||||
|
||||
| Level | Use |
|
||||
@@ -146,6 +167,9 @@ a separate concern that would produce 502/504 responses in the proxy handler.
|
||||
|
||||
## Systemd Integration
|
||||
|
||||
The proxy can also run as a bare binary via systemd (alternative to container
|
||||
deployment). The systemd unit file is provided for this use case.
|
||||
|
||||
### Unit File
|
||||
|
||||
```ini
|
||||
@@ -244,10 +268,181 @@ reverse-proxy [OPTIONS]
|
||||
Options:
|
||||
--config <PATH> Path to config file [default: /etc/reverse-proxy/config.toml]
|
||||
--validate Validate config and exit
|
||||
--allow-wildcard-bind Permit 0.0.0.0 as a bind address (for container deployments)
|
||||
--help Show help
|
||||
--version Show version
|
||||
```
|
||||
|
||||
## Container Deployment
|
||||
|
||||
### Rationale
|
||||
|
||||
The proxy runs in a minimal Docker container for defense-in-depth. Even if an
|
||||
attacker finds a logic-level vulnerability, they must also escape the container
|
||||
boundary. Combined with Rust's memory safety, this provides two independent
|
||||
barriers against exploitation. See ADR-020 for the full rationale.
|
||||
|
||||
### Container Image
|
||||
|
||||
Multi-stage build: compile in `rust:alpine`, run in `alpine` (or `scratch` for
|
||||
absolute minimum). The final image contains only the static binary and
|
||||
necessary runtime files. No shell, no package manager, no unnecessary tools.
|
||||
|
||||
The binary is compiled against the `x86_64-unknown-linux-musl` target for
|
||||
static linking. The `aws_lc_rs` crypto provider is statically linked — no
|
||||
OpenSSL dependency.
|
||||
|
||||
### Networking
|
||||
|
||||
The proxy supports flexible upstream addressing — no assumption about upstream
|
||||
localality:
|
||||
|
||||
| Deployment | Upstream Address | Example |
|
||||
|------------|-----------------|---------|
|
||||
| Same-host, shared Docker network | Docker DNS name | `gitea:3000` |
|
||||
| Same-host, host networking | Loopback | `127.0.0.1:3000` |
|
||||
| Different host, LAN | LAN IP | `10.0.0.5:3000` |
|
||||
| Different host, VPN/tunnel | Tunnel endpoint | Varies by tunnel config |
|
||||
|
||||
In container deployments, the proxy binds `0.0.0.0` inside the container and
|
||||
Docker publishes specific ports to the host IP. The `allow_wildcard_bind`
|
||||
override is required for this configuration (see ADR-016, ADR-020).
|
||||
|
||||
### Volume Mounts
|
||||
|
||||
| Container Path | Host Path | Purpose |
|
||||
|---------------|-----------|---------|
|
||||
| `/etc/reverse-proxy/config.toml` | Config file (read-only) | Proxy configuration |
|
||||
| `/var/lib/reverse-proxy/acme-cache/` | ACME state directory | Certificate persistence across restarts |
|
||||
| `/var/log/reverse-proxy/` | Log directory | fail2ban reads from host |
|
||||
| `/run/reverse-proxy/admin.sock` | Admin socket | Host-side config reload commands |
|
||||
|
||||
### Docker Compose Example
|
||||
|
||||
This example shows the reverse proxy alongside a Gitea container on a shared
|
||||
Docker network. Real IPs, secrets, and domain names are replaced with
|
||||
placeholders.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
reverse-proxy:
|
||||
build: .
|
||||
container_name: reverse-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "203.0.113.10:80:80" # HTTP redirect
|
||||
- "203.0.113.10:443:443" # HTTPS
|
||||
volumes:
|
||||
- /etc/reverse-proxy/config.toml:/etc/reverse-proxy/config.toml:ro
|
||||
- /var/lib/reverse-proxy/acme-cache:/var/lib/reverse-proxy/acme-cache
|
||||
- /var/log/reverse-proxy:/var/log/reverse-proxy
|
||||
- /run/reverse-proxy:/run/reverse-proxy
|
||||
networks:
|
||||
- proxy-net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9900/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "203.0.113.10:22:2222" # Git SSH
|
||||
volumes:
|
||||
- /opt/gitea:/data
|
||||
networks:
|
||||
- proxy-net
|
||||
- gitea-db-net
|
||||
|
||||
gitea-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: gitea-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: gitea
|
||||
volumes:
|
||||
- gitea-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- gitea-db-net
|
||||
|
||||
networks:
|
||||
proxy-net:
|
||||
gitea-db-net:
|
||||
|
||||
volumes:
|
||||
gitea-db:
|
||||
```
|
||||
|
||||
Corresponding proxy config (inside the container):
|
||||
|
||||
```toml
|
||||
allow_wildcard_bind = true
|
||||
health_check_port = 9900
|
||||
admin_socket_path = "/run/reverse-proxy/admin.sock"
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
format = "text"
|
||||
log_file_path = "/var/log/reverse-proxy/access.log"
|
||||
|
||||
[rate_limit]
|
||||
requests_per_second = 10
|
||||
burst = 20
|
||||
|
||||
[body]
|
||||
limit_bytes = 104857600
|
||||
|
||||
[[listeners]]
|
||||
bind_addr = "0.0.0.0"
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
|
||||
[listeners.tls]
|
||||
mode = "acme"
|
||||
acme_domains = ["git.example.com"]
|
||||
acme_cache_dir = "/var/lib/reverse-proxy/acme-cache"
|
||||
acme_directory = "production"
|
||||
|
||||
[[listeners.sites]]
|
||||
host = "git.example.com"
|
||||
upstream = "gitea:3000" # Docker DNS resolves this
|
||||
```
|
||||
|
||||
### fail2ban Integration
|
||||
|
||||
In container deployments, fail2ban runs on the host and reads the proxy's log
|
||||
file from the volume mount:
|
||||
|
||||
```
|
||||
/var/log/reverse-proxy/access.log → fail2ban filter → iptables/nftables
|
||||
```
|
||||
|
||||
This is simpler and more reliable than parsing Docker log drivers. The log
|
||||
file is the authoritative source for rate limit events and access logs.
|
||||
|
||||
### Health Check
|
||||
|
||||
Docker's native `HEALTHCHECK` uses the local health endpoint:
|
||||
|
||||
```dockerfile
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD wget -q --spider http://127.0.0.1:9900/health || exit 1
|
||||
```
|
||||
|
||||
No port publishing is needed — the health check runs inside the container.
|
||||
|
||||
### SSH Traffic
|
||||
|
||||
SSH traffic for Git operations is not proxied through the reverse proxy. It
|
||||
continues to be routed directly to the Gitea container via Docker port
|
||||
publishing (e.g., `203.0.113.10:22:2222`), matching the current deployment
|
||||
pattern.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
@@ -260,6 +455,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
| [009](decisions/009-signal-handling.md) | Signal handling strategy | signal-hook for SIGTERM/SIGINT/SIGHUP |
|
||||
| [013](decisions/013-health-check-port.md) | Health check on separate local port | Localhost-only HTTP health check, configurable port |
|
||||
| [014](decisions/014-unix-socket-reload.md) | Unix domain socket config reload API | Programmatic reload with success/failure feedback |
|
||||
| [020](decisions/020-container-deployment.md) | Container deployment model | Defense-in-depth via container isolation; file-primary logging |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -81,36 +81,51 @@ details.
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ reverse-proxy (Rust/axum) │
|
||||
config.toml ───────► │ StaticConfig + DynamicConfig │
|
||||
│ (ArcSwap for hot-reload) │
|
||||
│ │
|
||||
│ ┌─ Listener 1 ─────────────────┐ │
|
||||
bind_addr_1:80 ───► │ │ HTTP → 301 redirect │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
bind_addr_1:443 ───► │ │ TLS listener (tokio-rustls) │ │
|
||||
│ │ ├─ ACME or Manual TLS config │ │
|
||||
│ │ └─ axum router │ │
|
||||
│ │ ├─ Host-based routing │ │
|
||||
│ │ ├─ git.alk.dev → :3000 │ │
|
||||
│ │ └─ Rate limiting, headers │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Listener N ─────────────────┐ │
|
||||
bind_addr_N:80 ───► │ │ HTTP → 301 redirect │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
bind_addr_N:443 ───► │ │ TLS listener (tokio-rustls) │ │
|
||||
│ │ ├─ Manual TLS cert │ │
|
||||
│ │ └─ axum router │ │
|
||||
│ │ ├─ alk.dev → :8080 │ │
|
||||
│ │ └─ Rate limiting, headers │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ /health → 200 OK (port 9900) │
|
||||
└────────────────────────────────────┘
|
||||
┌────────────────────────────────────┐
|
||||
│ reverse-proxy container (Rust/axum)│
|
||||
config.toml ───────► │ StaticConfig + DynamicConfig │
|
||||
(volume mount) │ (ArcSwap for hot-reload) │
|
||||
│ │
|
||||
│ ┌─ Listener 1 ─────────────────┐ │
|
||||
bind_addr:80 ────► │ │ HTTP → 301 redirect │ │
|
||||
(published) │ └────────────────────────────────┘ │
|
||||
│ │
|
||||
bind_addr:443 ────► │ │ TLS listener (tokio-rustls) │ │
|
||||
(published) │ │ ├─ ACME or Manual TLS config │ │
|
||||
│ │ └─ axum router │ │
|
||||
│ │ ├─ Host-based routing │ │
|
||||
│ │ ├─ git.alk.dev → gitea:3000 │ │
|
||||
│ │ └─ Rate limiting, headers │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ Listener N ─────────────────┐ │
|
||||
bind_addr_N:80 ───► │ │ HTTP → 301 redirect │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
bind_addr_N:443 ───► │ │ TLS listener (tokio-rustls) │ │
|
||||
│ │ ├─ Manual TLS cert │ │
|
||||
│ │ └─ axum router │ │
|
||||
│ │ ├─ alk.dev → app:8080 │ │
|
||||
│ │ └─ Rate limiting, headers │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ /health → 200 OK (port 9900) │
|
||||
└────────────────────────────────────┘
|
||||
│ │
|
||||
┌──────┘ └──────┐
|
||||
│ │
|
||||
Docker network Volume mounts:
|
||||
(upstream DNS) ├─ config (ro)
|
||||
├─ gitea:3000 ├─ ACME cache (rw)
|
||||
├─ app:8080 ├─ log dir (rw, fail2ban)
|
||||
└─ admin socket (rw)
|
||||
```
|
||||
|
||||
In container deployments (ADR-020), the proxy runs in a minimal container with
|
||||
`0.0.0.0` bind address and Docker port publishing. Upstream addresses use Docker
|
||||
DNS names for same-host containers (e.g., `gitea:3000`) but also support
|
||||
loopback, LAN, and tunnel endpoints for multi-host deployments.
|
||||
|
||||
## Crate Dependencies
|
||||
|
||||
### Core
|
||||
@@ -180,6 +195,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
| [017](decisions/017-upstream-connection-defaults.md) | Upstream connection defaults | HTTP/1.1, no redirects, connection pooling |
|
||||
| [018](decisions/018-body-size-limit.md) | Request body size limit | 100 MB default matching nginx, Gitea push compatibility |
|
||||
| [019](decisions/019-multi-config-listeners.md) | Multi-config listeners | `[[listeners]]` supporting both dedicated-IP and shared-IP deployment models |
|
||||
| [020](decisions/020-container-deployment.md) | Container deployment model | Defense-in-depth via container isolation; file-primary logging; flexible upstream addressing |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ from axum's `ConnectInfo` and the original request:
|
||||
| `Host` | Original request `Host` header | Already present; preserved as-is |
|
||||
| `X-Real-IP` | `ConnectInfo<SocketAddr>` remote IP | Set to client's IP address |
|
||||
| `X-Forwarded-For` | Client IP, appended if header exists | Comma-separated list of proxies |
|
||||
| `X-Forwarded-Proto` | Determined by listener | `https` on port 443, `http` on port 80 |
|
||||
| `X-Forwarded-Proto` | Determined by which listener port received the request | `https` for requests on the listener's `https_port`, `http` for requests on the listener's `http_port` |
|
||||
|
||||
The `X-Forwarded-For` handling must append the client IP to any existing value
|
||||
(rather than replacing it), to support chained proxies correctly.
|
||||
|
||||
Reference in New Issue
Block a user