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.
8.0 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-11 |
Overview
Vision
A memory-safe, minimal reverse proxy that replaces our vulnerable nginx instance for forward-proxying to backend services. The proxy terminates TLS, injects standard proxy headers, enforces rate limits, and forwards requests to upstream services — with operational feature parity for our current single-domain Gitea setup.
Why This Exists
Our nginx 1.24.0 installation is vulnerable to multiple actively-exploited
CVEs, including CVE-2026-42945 (unauthenticated RCE via rewrite/set
directives). The broader threat landscape is worsening: LLM-assisted fuzzing
is accelerating bug discovery in nginx's C codebase, and security researchers
report additional undisclosed vulnerabilities. Upgrading nginx patches known
CVEs but does not address the structural problem — memory corruption bugs are
endemic to C, and the discovery rate is accelerating.
Rust's memory safety eliminates the entire class of buffer overflow, use-after-free, and double-free bugs that constitute 6 of 7 recent nginx CVEs. Combined with rustls (pure Rust TLS, no OpenSSL dependency), this provides a fundamentally safer baseline.
See threat-landscape.md for full vulnerability details.
Scope
In Scope
-
Phase 1: Replace nginx for
git.alk.devwith feature parity- TLS termination with ACME (Let's Encrypt) certificate management
- Manual certificate paths as fallback mode
- HTTP → HTTPS redirect
- Reverse proxy to Gitea at
127.0.0.1:3000 - Proxy header injection (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto)
- Request rate limiting with fail2ban-compatible logging (global per-IP; per-site in Phase 2)
- 100 MB body size limit (global; per-site in Phase 2)
- Configurable bind address (no
0.0.0.0default) - Health check endpoint
- Graceful shutdown (SIGTERM handling)
- Systemd unit file
-
Phase 2: Multi-site support
- SNI-based TLS routing for multiple domains
- Config file for site definitions
- Dynamic config reload (ArcSwap pattern)
-
Phase 3: Operational hardening
- Metrics endpoint (Prometheus-compatible)
- Connection limits and timeouts
- Log rotation
Out of Scope
- HTTP/2 or HTTP/3 proxying (services that need these run their own native
Rust servers — e.g.,
api.alk.dev) - Load balancing or round-robin upstream selection
- WebSocket proxying (can be added later if needed)
- Static file serving
- Access control beyond rate limiting (no auth, no IP allowlists in Phase 1)
- CGI, SCGI, uWSGI, FastCGI
Architecture
┌────────────────────────────────────┐
│ reverse-proxy (Rust/axum) │
config.toml ──────► │ StaticConfig + DynamicConfig │
│ (ArcSwap for hot-reload) │
│ │
bind_addr:80 ──► │ HTTP listener → 301 redirect │
│ to HTTPS │
│ │
bind_addr:443 ──► │ TLS listener (tokio-rustls) │
│ ├─ ACME mode: rustls-acme resolver │
│ │ (auto cert provisioning/renewal) │
│ └─ Manual mode: cert/key file paths │
│ │
│ axum router │
│ ├─ Host-based routing │
│ ├─ Rate limiting middleware │
│ ├─ Proxy header injection │
│ ├─ Body size limit (100MB) │
│ └─ Reverse proxy handler │
│ └─ hyper Client → upstream │
│ │
│ /health → 200 OK │
└────────────────────────────────────┘
Crate Dependencies
Core
| Crate | Version | Purpose | Notes |
|---|---|---|---|
axum |
0.8 | HTTP framework | Routing, middleware, extractors |
tokio |
1 (full) | Async runtime | Multi-threaded runtime |
hyper |
1 | HTTP protocol | Used via axum, and directly for proxy Client |
tower |
0.5 | Middleware ecosystem | Service trait, layers |
rustls |
0.23 | TLS implementation | aws_lc_rs crypto provider |
tokio-rustls |
0.26 | Async TLS I/O | Wraps TCP with TLS |
rustls-acme |
0.12 | ACME client | Let's Encrypt auto-provisioning and renewal |
Supporting
| Crate | Version | Purpose | Notes |
|---|---|---|---|
serde |
1 | Serialization | TOML config deserialization |
toml |
0.8 | Config format | Declarative site definitions |
arc-swap |
1 | Atomic config swap | Lock-free DynamicConfig reload |
tracing |
0.1 | Structured logging | fail2ban-compatible output |
tracing-subscriber |
0.3 | Log output | File + journald support |
rustls-pemfile |
2 | PEM parsing | Manual cert loading |
rustls-pki-types |
1 | TLS types | CertificateDer, PrivateKeyDer |
clap |
4 | CLI arguments | Server startup options |
signal-hook |
0.3 | Signal handling | SIGTERM/SIGINT for shutdown, SIGHUP for config reload |
Versions listed are minimum major versions. Implementation should pin exact
versions in Cargo.toml per standard Rust practice.
Exports
This is a single-binary deployment. There are no library exports. The product
is the reverse-proxy binary plus a systemd unit file and a config file.
Dependencies on Other Projects
- alknet: The
ArcSwap<DynamicConfig>pattern,tokio-rustlsTLS acceptor construction,rustls-acmeintegration, andServerConfigbuilder patterns are adapted from alknet's transport and config layers. These patterns are referenced as validation that the approaches work in production; all code in this project is written from scratch.
Design Decisions
All design decisions are documented as ADRs in decisions/.
| ADR | Decision | Summary |
|---|---|---|
| 001 | Rust with axum | Memory safety eliminates the bug class causing nginx CVEs; axum provides ergonomic tower integration |
| 002 | Custom proxy handler | Single upstream, single domain — axum-reverse-proxy adds unnecessary complexity |
| 003 | TOML configuration format | Rust-native, unambiguous, excellent serde support |
| 004 | ACME-primary certificate management | Eliminates certbot dependency; automatic provisioning and renewal |
| 005 | tokio-rustls directly, not axum-server | Full control over TLS config, ACME resolver integration, cipher suite configuration |
| 006 | Token bucket rate limiting | In-memory per-IP token bucket matching nginx burst semantics |
| 007 | Custom structured log format | key=value pairs with RATE_LIMIT prefix for fail2ban |
| 008 | Static/dynamic config with ArcSwap | Immutable StaticConfig, hot-reloadable DynamicConfig via ArcSwap |
| 009 | Signal handling strategy | signal-hook for SIGTERM/SIGINT/SIGHUP |
Open Questions
Open questions are tracked in open-questions.md. Key questions affecting this document:
- OQ-01: Should cipher suites be restricted beyond rustls defaults? (open)
- OQ-03: Should the health check endpoint be on a separate port? (open)
- OQ-05: Should the proxy bind to multiple addresses or just one? (open)