Add YAML-configured ACLs with Basic and IP authorization
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Hermes
|
||||
|
||||
Hermes is a minimal HTTP/1.1 forward proxy written in Rust. It supports streaming plain HTTP requests and HTTPS tunneling with `CONNECT`, with OpenTelemetry traces, metrics, and logs exported over OTLP/HTTP.
|
||||
Hermes is a minimal HTTP/1.1 forward proxy written in Rust. It supports streaming plain HTTP requests, HTTPS tunneling with `CONNECT`, YAML-configured ACLs, and OpenTelemetry traces, metrics, and logs exported over OTLP/HTTP.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -8,10 +8,12 @@ Hermes is a minimal HTTP/1.1 forward proxy written in Rust. It supports streamin
|
||||
- HTTPS and arbitrary TCP tunneling with `CONNECT host:port`
|
||||
- Streaming request and response bodies
|
||||
- Upstream connection pooling and configurable timeouts
|
||||
- Ordered allow/deny ACLs using Basic authentication and/or client IP networks
|
||||
- Salted Argon2id password hashes; plaintext passwords are never stored in YAML
|
||||
- Removal of standard and `Connection`-nominated hop-by-hop headers
|
||||
- W3C Trace Context extraction and injection for plain HTTP traffic
|
||||
- Structured, trace-correlated logs on stdout and through OTLP
|
||||
- Bounded-cardinality HTTP and tunnel metrics
|
||||
- Bounded-cardinality HTTP, tunnel, and ACL metrics
|
||||
- Localhost-only binding by default
|
||||
|
||||
TLS is not terminated or inspected. HTTPS remains end-to-end encrypted between the client and destination.
|
||||
@@ -20,10 +22,10 @@ TLS is not terminated or inspected. HTTPS remains end-to-end encrypted between t
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo run
|
||||
cargo run -- --config examples/hermes.yaml
|
||||
```
|
||||
|
||||
The proxy listens on `127.0.0.1:8080` by default.
|
||||
The default configuration path is `hermes.yaml`. The included example listens on `127.0.0.1:8080` and allows only localhost clients.
|
||||
|
||||
```sh
|
||||
# Plain HTTP
|
||||
@@ -33,16 +35,74 @@ curl --proxy http://127.0.0.1:8080 http://example.com/
|
||||
curl --proxy http://127.0.0.1:8080 https://example.com/
|
||||
```
|
||||
|
||||
### Proxy configuration
|
||||
## YAML configuration
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
```yaml
|
||||
listen_addr: "127.0.0.1:8080"
|
||||
connect_timeout_ms: 10000
|
||||
upstream_timeout_ms: 30000
|
||||
|
||||
acl:
|
||||
default_action: deny
|
||||
|
||||
users:
|
||||
alice:
|
||||
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$..."
|
||||
|
||||
rules:
|
||||
- name: alice
|
||||
action: allow
|
||||
users: ["alice"]
|
||||
|
||||
- name: office-network
|
||||
action: allow
|
||||
networks: ["10.20.0.0/16", "2001:db8:20::/48"]
|
||||
```
|
||||
|
||||
| Field | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `HERMES_LISTEN_ADDR` | `127.0.0.1:8080` | Downstream listen address |
|
||||
| `HERMES_CONNECT_TIMEOUT_MS` | `10000` | TCP connection timeout for `CONNECT` and the HTTP connector |
|
||||
| `HERMES_UPSTREAM_TIMEOUT_MS` | `30000` | Time limit for receiving upstream HTTP response headers |
|
||||
| `RUST_LOG` | `info` | `tracing` filter, for example `hermes=debug` |
|
||||
| `listen_addr` | `127.0.0.1:8080` | Downstream listen address |
|
||||
| `connect_timeout_ms` | `10000` | TCP connection timeout for `CONNECT` and the HTTP connector |
|
||||
| `upstream_timeout_ms` | `30000` | Time limit for receiving upstream HTTP response headers |
|
||||
| `acl.default_action` | `deny` | Action when no rule matches |
|
||||
| `acl.users` | empty | Usernames and Argon2id PHC password hashes |
|
||||
| `acl.rules` | empty | Ordered ACL rules |
|
||||
|
||||
Do not expose Hermes on a public interface without adding appropriate authentication and destination access controls. The default configuration intentionally binds to localhost.
|
||||
Configuration is loaded and validated at startup. Unknown fields, invalid CIDRs, duplicate or empty rules, references to unknown users, and unsupported password hashes prevent startup. Restart Hermes to apply configuration changes.
|
||||
|
||||
`RUST_LOG` controls log filtering and defaults to `info`, for example `RUST_LOG=hermes=debug`.
|
||||
|
||||
### ACL rules
|
||||
|
||||
- Rules are evaluated in order; the **first match wins**.
|
||||
- Separate user and network rules provide Basic-auth OR IP-address authorization.
|
||||
- If a rule contains both `users` and `networks`, both selectors must match. Entries within each selector are ORed.
|
||||
- A rule must contain at least one selector; accidental match-all rules are rejected.
|
||||
- IP matching uses the actual TCP peer address. `X-Forwarded-For` is never trusted.
|
||||
- Requests without required credentials receive `407 Proxy Authentication Required` and a Basic `Proxy-Authenticate` challenge.
|
||||
- Invalid credentials receive 407; authenticated or IP-identified clients denied by policy receive 403.
|
||||
- ACLs are enforced before upstream HTTP requests or `CONNECT` connections are made.
|
||||
- `Proxy-Authorization` is consumed by Hermes and is never forwarded upstream.
|
||||
|
||||
For Basic authentication, curl can supply proxy credentials with `--proxy-user`:
|
||||
|
||||
```sh
|
||||
curl --proxy http://127.0.0.1:8080 \
|
||||
--proxy-user 'alice:your-password' \
|
||||
https://example.com/
|
||||
```
|
||||
|
||||
### Generate a password hash
|
||||
|
||||
```sh
|
||||
cargo run -- hash-password
|
||||
```
|
||||
|
||||
The helper prompts without echoing, confirms the password, and prints a PHC string such as `$argon2id$v=19$...`. Copy the complete string into the user's `password_hash` field. The PHC string includes a cryptographically random salt, the Argon2id parameters, and the hash; no separate salt field is needed. Running the helper twice for the same password produces different salted hashes.
|
||||
|
||||
Password verification uses bounded blocking work so Argon2 does not stall Tokio's async runtime. Decoded password buffers are zeroized after use.
|
||||
|
||||
**Basic authentication is not encrypted in transit.** Argon2 protects the stored password hash, but a client sending Basic credentials over cleartext HTTP exposes them to the network. Use Basic authentication only on localhost or a trusted private network unless downstream TLS is added. Do not expose Hermes publicly without appropriate network controls.
|
||||
|
||||
## OpenTelemetry
|
||||
|
||||
@@ -51,10 +111,10 @@ Hermes exports traces, metrics, and logs independently to an OpenTelemetry Colle
|
||||
```sh
|
||||
export OTEL_SERVICE_NAME=hermes
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
|
||||
cargo run
|
||||
cargo run -- --config examples/hermes.yaml
|
||||
```
|
||||
|
||||
Signal-specific settings such as `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` are also supported by the exporter. Standard OTLP headers, timeouts, and trace sampling environment variables can be used.
|
||||
Signal-specific settings such as `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` are also supported. Standard OTLP headers, timeouts, and trace sampling environment variables can be used.
|
||||
|
||||
Traces and logs are batched, metrics are exported periodically, and all providers are flushed during shutdown. An unavailable collector does not prevent the proxy from forwarding traffic, although exporter errors may appear in stdout logs.
|
||||
|
||||
@@ -78,11 +138,11 @@ In another terminal, run Hermes and send a request through it. The collector wil
|
||||
- `proxy.connect`: `CONNECT` handshake and tunnel lifetime
|
||||
- `proxy.upstream_connect`: upstream TCP connection establishment
|
||||
|
||||
For plain HTTP, incoming W3C `traceparent` and `tracestate` headers establish the parent context, and a new context is injected upstream. Trace context cannot be propagated inside an encrypted `CONNECT` tunnel.
|
||||
For plain HTTP, incoming W3C `traceparent` and `tracestate` headers establish the parent context, and a new context is injected upstream. Trace context cannot be propagated inside an encrypted `CONNECT` tunnel. Request spans include the ACL decision, reason, and matched rule name without credentials.
|
||||
|
||||
### Metrics
|
||||
|
||||
Hermes uses standard HTTP instrument names where applicable and adds proxy-specific tunnel instruments:
|
||||
Hermes uses standard HTTP instrument names where applicable and adds proxy-specific instruments:
|
||||
|
||||
- `hermes.proxy.requests`
|
||||
- `http.server.request.duration`
|
||||
@@ -92,17 +152,18 @@ Hermes uses standard HTTP instrument names where applicable and adds proxy-speci
|
||||
- `hermes.proxy.tunnels.active`
|
||||
- `hermes.proxy.tunnel.duration`
|
||||
- `hermes.proxy.tunnel.bytes`
|
||||
- `hermes.proxy.acl.decisions`
|
||||
|
||||
Metric attributes are deliberately bounded. Raw URLs, destination hostnames, and client addresses are not metric attributes.
|
||||
Metric attributes are deliberately bounded. Raw URLs, destination hostnames, usernames, rule names, and client addresses are not metric attributes.
|
||||
|
||||
### Logs
|
||||
|
||||
Startup, shutdown, request failures, and tunnel completion are emitted as structured `tracing` events. Logs emitted inside a trace span include its trace and span IDs in OTLP. Headers, bodies, credentials, and query strings are not logged by the proxy.
|
||||
Startup, shutdown, ACL decisions, request completion, request failures, and tunnel completion are emitted as structured `tracing` events. Logs emitted inside a trace span include its trace and span IDs in OTLP. Headers, bodies, passwords, authorization values, password hashes, and query strings are not logged by the proxy.
|
||||
|
||||
## Limitations
|
||||
|
||||
- HTTP/1.1 downstream and upstream only; no HTTP/2 or gRPC proxying
|
||||
- No TLS interception, caching, authentication, ACLs, or rate limiting
|
||||
- No downstream TLS, TLS interception, caching, rate limiting, or configuration hot reload
|
||||
- No HTTP Upgrade forwarding other than `CONNECT`
|
||||
- No dynamic configuration or Envoy xDS support
|
||||
|
||||
|
||||
Reference in New Issue
Block a user