Phase 1 architecture docs covering proxy handler, TLS termination (ACME + manual), TOML config with static/dynamic split (ArcSwap), and operations (rate limiting, logging, health check, systemd, graceful shutdown). Nine ADRs documenting key decisions: Rust/axum, custom proxy handler, TOML config, rustls-acme for cert management, tokio-rustls direct, token bucket rate limiting, custom log format for fail2ban, static/dynamic config split, and signal handling strategy. Includes threat landscape research documenting the nginx CVEs motivating this project.
2.8 KiB
ADR-008: Static/Dynamic Configuration Split with ArcSwap
Status
Accepted
Context
The proxy needs configuration that can be partially reloaded at runtime (site definitions, rate limits) without restarting the process and dropping active connections. However, some configuration (bind addresses, TLS mode) fundamentally requires creating new listeners and cannot be changed at runtime.
Two approaches:
- Full restart for all config changes: Simple, but requires dropping active connections for every change, including trivial rate limit adjustments.
- Static/dynamic split: Immutable parameters (bind address, TLS mode) in a
StaticConfigthat requires restart; runtime-adjustable parameters (sites, rate limits) in aDynamicConfigthat can be atomically swapped viaArc<ArcSwap<DynamicConfig>>without dropping connections.
This pattern is proven in the alknet project, which uses the same
ArcSwap<DynamicConfig> approach for auth policy, forwarding rules, and rate
limits.
Decision
Split configuration into StaticConfig (immutable after startup) and
DynamicConfig (hot-reloadable via ArcSwap). The split is:
StaticConfig (restart required):
- Bind address, HTTP port, HTTPS port
- TLS mode (ACME vs. manual), cert paths, ACME settings
- Log level and format
DynamicConfig (hot-reloadable via SIGHUP):
- Site definitions (hostname → upstream mappings)
- Rate limits (requests per second, burst)
- Body size limits
ConfigReloadHandle provides a reload(DynamicConfig) method that atomically
swaps the entire config. All request handlers read DynamicConfig via
ArcSwap::load() — a lock-free operation.
Rationale
- Rate limits and site definitions change more frequently than bind addresses and TLS settings. Hot-reload avoids unnecessary downtime.
ArcSwapprovides lock-free reads and atomic writes — no partial updates, no lock contention on the hot path.- Proven pattern from alknet, where it's used for auth policy, forwarding rules, and rate limits.
- SIGHUP trigger is simple, well-understood, and compatible with systemd and process supervisors.
- The entire config is swapped at once, preventing inconsistent states where some sites use the old config and others use the new one.
Consequences
Positive:
- Zero-downtime config reload for sites and rate limits
- Lock-free reads on the request hot path
- Atomic config updates — no partial states
- Proven pattern from alknet
Negative:
- Two config types add conceptual complexity
- SIGHUP reload requires reading the config file from disk (need to handle file read errors gracefully)
- Must validate DynamicConfig before swapping (invalid config must not replace valid config)
References
- config.md
- alknet ADR-030 (static/dynamic config split)