v0.26.0 proxy headers for acl
This commit is contained in:
1 parent
4af072f4e5
commit
ee01cb00fe
21 files changed
+1115
-298
No files matched your search
+2
-2
@@ -30,8 +30,8 @@ streams, or downstream LogWisp nodes.
|
||||
- [CLI](cli.md): flags, signals, exit codes, `lw auth`; also the `lw(1)`
|
||||
manual, [`lw.1`](lw.1)
|
||||
- [Operations](operations.md): running, monitoring, tuning, troubleshooting
|
||||
- [To Do](todo.md): planned work in priority order: network access control
|
||||
and the PROXY protocol, packaging, smaller follow-ups
|
||||
- [To Do](todo.md): planned work in priority order: per-client limits,
|
||||
packaging, smaller follow-ups
|
||||
|
||||
A fully annotated configuration covering every option lives at
|
||||
[`config/logwisp.toml`](../config/logwisp.toml).
|
||||
|
||||
+3
-2
@@ -167,8 +167,9 @@ TLS is built in exactly one place, `internal/tlsx`, which exposes
|
||||
Each pipeline owns a `session.Manager`. Plugins receive a `session.Proxy`
|
||||
scoped to their instance id, so one plugin cannot see or remove another's
|
||||
sessions. A session records the remote address, creation and last-activity
|
||||
timestamps, and metadata — including `tls` and `tls_peer_cn` for TLS peers, and
|
||||
`auth_method` / `auth_identity` for authorized ones.
|
||||
timestamps, and metadata — including `tls` and `tls_peer_cn` for TLS peers,
|
||||
`auth_method` / `auth_identity` for authorized ones, and `peer_addr`, the proxy,
|
||||
for a client a PROXY header names.
|
||||
|
||||
Idle sessions are reaped every 5 minutes against a 30-minute idle limit. The
|
||||
HTTP sink's broker treats a vanished session as an eviction signal and closes
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ every key.
|
||||
- filters, rate limits, heartbeat, formatter flags and timestamp layouts
|
||||
- more than one pipeline; both chain transports, or a further hop, on one node
|
||||
- proxy mode: browser logins through a TLS-terminating proxy
|
||||
- `acl` address rules on the listeners
|
||||
- `acl` on the listeners: address rules, PROXY headers
|
||||
- mtls identities other than the certificate CN; `node_binding` other than
|
||||
`force`
|
||||
- creating certificates
|
||||
|
||||
+4
-1
@@ -312,4 +312,7 @@ or SKIP (exit 77, e.g. no IPv6 loopback); only a failure fails the target.
|
||||
- `deploy-test.sh`: the configurations `deploy/lw-deploy.sh` writes, deployed
|
||||
and checked; ports 15871-15879
|
||||
- `acl-test.sh`: each listener admitting or refusing peers by address before
|
||||
TLS, with clients from 127.0.0.1 and 127.0.0.2; ports 15881-15889
|
||||
TLS, with clients from 127.0.0.1 and 127.0.0.2, then by the client a PROXY
|
||||
header names behind a stub proxy on 127.0.0.11-13 (and nginx `stream` on
|
||||
127.0.0.14 when installed), per-client login throttling included; ports
|
||||
15881-15889; skips (exit 77) where 127.0.0.2 cannot be bound
|
||||
+29
-22
@@ -208,13 +208,22 @@ headers, and entry encoding.
|
||||
- Check the port is not already bound by another pipeline in the same process.
|
||||
|
||||
**Closed as soon as it connects, before TLS**
|
||||
- The listener's `acl` refused the address: `acl_denied` rises, and its log has
|
||||
`Connection refused by acl` at WARN, once a minute at most. The dialer sees
|
||||
`EOF` or `connection reset by peer` (curl: exit 52 or 56).
|
||||
- Behind a proxy (an L4 passthrough, or the `http` sink's
|
||||
`auth.trusted_proxies`) every peer has the proxy's address: allow the proxy.
|
||||
- The listener's `acl` refused it: `acl_denied` rises, and its log has
|
||||
`Connection refused by acl` at WARN with a `reason`, once a minute at most.
|
||||
The dialer sees `EOF` or `connection reset by peer` (curl: exit 52 or 56).
|
||||
- `address rules` — `allow` or `deny` excludes the client: the one a PROXY
|
||||
header names (`remote_addr`, the proxy as `peer_addr`), else the socket
|
||||
peer. Behind the `http` sink's `auth.trusted_proxies` that is the proxy:
|
||||
allow it.
|
||||
- `no PROXY header` — a peer in `proxy_from` sent none under
|
||||
`proxy_protocol = "required"`: the proxy lacks `proxy_protocol on` (nginx)
|
||||
or `send-proxy` (HAProxy), or the proxy's host dialed directly.
|
||||
- `PROXY header: …` — malformed, too long, or incomplete after 10 s.
|
||||
- `PROXY header from a peer outside proxy_from` — at the first read, after
|
||||
the connection opened: list the proxy in `proxy_from`.
|
||||
- `acl: allow entry "...": not of the listener's family` at startup — an IPv4
|
||||
entry on an IPv6 `host`, or the reverse: each family's listener takes its own.
|
||||
Behind `proxy_from`, `allow` and `deny` take both; `proxy_from` its own.
|
||||
|
||||
**TLS handshake failure**
|
||||
- `client didn't provide a certificate` — the listener has `client_auth = true`
|
||||
@@ -271,9 +280,9 @@ headers, and entry encoding.
|
||||
`X-Forwarded-For`.
|
||||
- `too many attempts` — the address failed or abandoned logins faster than one
|
||||
per second beyond a burst of 10, or has 4 unfinished; it clears within seconds
|
||||
once the failing peer stops. Peers behind one NAT or one passthrough proxy
|
||||
share the budget. `busy` — 4,096 logins in flight, or the listener is
|
||||
stopping.
|
||||
once the failing peer stops. Peers behind one NAT, or one passthrough proxy
|
||||
that sends no PROXY header, share the budget. `busy` — 4,096 logins in
|
||||
flight, or the listener is stopping.
|
||||
- `authentication not enabled` — the dialer has `scram`; the listener's
|
||||
`auth.type` is `none` or `mtls`.
|
||||
- `no challenge within 10s (older logwisp, or not a scram listener)` — the
|
||||
@@ -301,20 +310,18 @@ headers, and entry encoding.
|
||||
proxy mode, the proxy gate.
|
||||
|
||||
**Behind an L4 proxy**
|
||||
- Every connection through the proxy fails: it sends a PROXY header (nginx
|
||||
`stream` with `proxy_protocol on`, HAProxy `send-proxy`) to a LogWisp TLS
|
||||
listener, which cannot read one yet. The header lands in front of the TLS
|
||||
handshake, which fails; the listener logs the proxy's address and
|
||||
`first record does not look like a TLS handshake` (the `tcp` sink at DEBUG).
|
||||
Route LogWisp without it: see
|
||||
[Behind nginx or another proxy](security.md#behind-nginx-or-another-proxy).
|
||||
- All peers share one throttling budget behind a passthrough proxy: without
|
||||
PROXY every `scram` peer arrives from the proxy's address. One client failing
|
||||
logins exhausts everyone's, and a fifth login while 4 are unfinished (edges
|
||||
reconnecting together after a restart) is refused; both answer
|
||||
`too many attempts` ([Throttling](security.md#throttling)).
|
||||
Expose the LogWisp ports directly, or keep the proxy hop on a trusted
|
||||
network so only trusted clients share the budget.
|
||||
- Every connection through the proxy fails, the listener logging the proxy's
|
||||
address and `first record does not look like a TLS handshake` (the `tcp`
|
||||
sink at DEBUG): the proxy sends a PROXY header (nginx `stream` with
|
||||
`proxy_protocol on`, HAProxy `send-proxy`) to a listener without
|
||||
`acl.proxy_protocol`. Set it, with the proxy in `proxy_from`: see
|
||||
[PROXY protocol](security.md#proxy-protocol).
|
||||
- All peers share one throttling budget and logs name the proxy: it sends no
|
||||
PROXY header, so every `scram` peer arrives from its address. One client
|
||||
failing logins exhausts everyone's, and a fifth login while 4 are unfinished
|
||||
(edges reconnecting together after a restart) is refused; both answer
|
||||
`too many attempts` ([Throttling](security.md#throttling)). Have the proxy
|
||||
send the header.
|
||||
|
||||
**Entries not arriving over a chain link**
|
||||
- Check the sink's `connected` statistic and its `reconnects` count.
|
||||
|
||||
@@ -374,8 +374,8 @@ and plaintext-forwarded requests `403`. The existing scripts keep passing.
|
||||
|
||||
## Not Implemented
|
||||
|
||||
1. **PROXY protocol behind TLS passthrough**: planned in
|
||||
[To Do, 1.2](todo.md#12-proxy-protocol-deferred-gap-of-scram-see-scram-auth-planmd).
|
||||
None. The PROXY protocol behind TLS passthrough, deferred here, shipped with
|
||||
the ACL work: [Security](security.md#proxy-protocol).
|
||||
|
||||
## mTLS Hardening
|
||||
|
||||
|
||||
+86
-64
@@ -26,7 +26,8 @@ configure it, and — equally important — what it does not yet do.
|
||||
[Browsers behind a TLS-terminating proxy](#browsers-behind-a-tls-terminating-proxy).
|
||||
- Server pinning by dialers: certificate identity (`mtls`), bound certificate
|
||||
(`scram`).
|
||||
- Address rules on every listener, applied before TLS: see
|
||||
- Address rules on every listener, applied before TLS, to the client a PROXY
|
||||
header (v1 or v2) from a listed L4 proxy names: see
|
||||
[The ACL Block](#the-acl-block).
|
||||
- Startup warnings for expiring certificates and risky settings: see
|
||||
[Startup Warnings](#startup-warnings).
|
||||
@@ -53,11 +54,8 @@ configure it, and — equally important — what it does not yet do.
|
||||
or credentials file.
|
||||
- Per-address connection or request limits: only SCRAM logins are throttled
|
||||
per address.
|
||||
- Address rules for clients behind a proxy: `acl` sees the proxy, never the
|
||||
client a PROXY header or `X-Forwarded-For` names.
|
||||
- PROXY protocol: behind a proxy that passes TLS through, every peer shares the
|
||||
proxy's address; see
|
||||
[Behind nginx or another proxy](#behind-nginx-or-another-proxy).
|
||||
- Address rules for `X-Forwarded-For` clients: in the `http` sink's proxy mode
|
||||
`acl` sees the L7 proxy, never the client it forwards.
|
||||
|
||||
Two credentials are supported. Certificates (`mtls`) are the one the transport
|
||||
already carries: the `tls` block establishes that a peer chains to your CA, and
|
||||
@@ -334,25 +332,63 @@ peer costs no handshake and never reaches `auth`.
|
||||
[pipelines.plugin_sources.config.acl]
|
||||
allow = ["192.0.2.0/24", "198.51.100.7"] # addresses or CIDRs; empty: all
|
||||
deny = ["192.0.2.66"] # refused, listed in allow or not
|
||||
proxy_protocol = "required" # off (default), optional, required
|
||||
proxy_from = ["10.0.0.5"] # L4 proxies sending PROXY headers
|
||||
```
|
||||
|
||||
- `deny` wins; then a non-empty `allow` admits only its entries, an empty one
|
||||
every address `deny` does not list.
|
||||
- Entries are of the listener's [family](networking.md#address-family): IPv4
|
||||
on an IPv4 listener, IPv6 on an IPv6 one, either behind a hostname (only the
|
||||
family it binds ever matches). An entry of the other family, an IPv4-mapped
|
||||
address, one with a zone, or anything but an address or CIDR fails
|
||||
construction. A link-local peer matches without its zone.
|
||||
family it binds ever matches). Behind `proxy_from` they take either, as a
|
||||
header may name a client of either. An entry of the other family, an
|
||||
IPv4-mapped address, one with a zone, or anything but an address or CIDR
|
||||
fails construction. A link-local peer matches without its zone.
|
||||
- A refused connection is closed at once and counted in `acl_denied` (plugin
|
||||
stats, the `http` sink's status). A WARN names the peer and the count, at
|
||||
most once a minute per listener; the refusals in between are only counted.
|
||||
- The rules see the socket peer: behind a proxy (an L4 passthrough, or the
|
||||
`http` sink's `trusted_proxies`), the proxy (see
|
||||
[Behind nginx or another proxy](#behind-nginx-or-another-proxy)).
|
||||
stats, the `http` sink's status). A WARN names the peer, the reason and the
|
||||
count, at most once a minute per listener; the refusals in between are only
|
||||
counted.
|
||||
- The rules see the client: the socket peer, or the client a PROXY header
|
||||
names. Behind the `http` sink's `trusted_proxies` they see the L7 proxy.
|
||||
- The `serve` and `aggregator` presets take `allow` and `deny` list keys.
|
||||
- Dialers have no `acl`: on a `tcp_chain` or `http_chain` sink it is an
|
||||
unknown key.
|
||||
|
||||
### PROXY protocol
|
||||
|
||||
Behind an L4 proxy that passes TLS through (nginx `stream`, HAProxy
|
||||
`mode tcp`) every peer comes from the proxy's address. With `proxy_protocol`
|
||||
the proxy names the client in a PROXY header, v1 (text) or v2 (binary), sent
|
||||
before the TLS ClientHello; LogWisp still ends TLS, so channel binding and
|
||||
client certificates are unchanged.
|
||||
|
||||
- `proxy_protocol` and `proxy_from` come together; `proxy_from` holds the
|
||||
proxies' addresses or CIDRs, of the listener's family. A peer with a zone
|
||||
(link-local IPv6) is never a proxy.
|
||||
- A peer in `proxy_from`:
|
||||
- sends its header within `tlsx.HandshakeTimeout` (10 s); a malformed,
|
||||
oversized (v1 over 107 bytes, a v2 block over 2048) or late one is
|
||||
refused;
|
||||
- under `required` is refused without a header; under `optional` it keeps
|
||||
its socket address, after up to 10 s if it sends nothing at first. A
|
||||
client the proxy forwards without a header then passes as the proxy, so
|
||||
`optional` (which warns at startup) suits only a rollout; set `required`
|
||||
once every route from `proxy_from` sends the header;
|
||||
- keeps its socket address for `LOCAL` (v2) and `UNKNOWN` (v1), the proxies'
|
||||
health checks, and for v2 families other than TCP over IPv4 and IPv6. TLVs
|
||||
are skipped.
|
||||
- Headers are read off the accept loop: a slow proxy holds only its own
|
||||
connection.
|
||||
- A peer not in `proxy_from` connects as itself; one that sends a header is
|
||||
refused at its first read, as any peer can write one.
|
||||
- The connection then reports the client (an IPv4-mapped one as IPv4) to the
|
||||
rules, SCRAM [throttling](#throttling), sessions and logs. Session metadata
|
||||
keeps the proxy as `peer_addr`, and a refusal's WARN names it.
|
||||
- `acl_proxy_headers` counts the headers read.
|
||||
- The `http` sink's proxy mode is the L7 counterpart: `trusted_proxies` trusts
|
||||
`X-Forwarded-For` from a proxy that ends TLS, `proxy_from` a PROXY header
|
||||
from one that passes it through.
|
||||
|
||||
## Startup Warnings
|
||||
|
||||
Some settings work but are usually mistakes. Each plugin reports them at WARN
|
||||
@@ -370,7 +406,9 @@ when it is constructed, so startup and every reload repeat them:
|
||||
(once per path per process)
|
||||
- an `acl` entry with host bits set (`10.0.0.1/8`), naming the network it
|
||||
matches; an `acl.allow` entry of a whole family (`0.0.0.0/0`, `::/0`); a
|
||||
wildcard listener whose `acl` has `deny` but no `allow`
|
||||
wildcard listener whose `acl` has `deny` but no `allow`; an `acl.proxy_from`
|
||||
entry reaching beyond private, loopback and link-local addresses;
|
||||
`acl.proxy_protocol = "optional"`
|
||||
|
||||
## Certificates made at startup
|
||||
|
||||
@@ -584,9 +622,10 @@ exchange.
|
||||
|
||||
### Throttling
|
||||
|
||||
Logins are throttled per remote socket address or, on an `http` sink in proxy
|
||||
mode, per forwarded client (an IPv6 client per /64); forwarded headers are read
|
||||
only from `trusted_proxies`. Each exchange takes a token from a bucket of 10
|
||||
Logins are throttled per client (an IPv6 client per /64): the socket peer, the
|
||||
client a [PROXY header](#proxy-protocol) names, or on an `http` sink in proxy
|
||||
mode the forwarded client; forwarded headers are read only from
|
||||
`trusted_proxies`. Each exchange takes a token from a bucket of 10
|
||||
that refills at one per second, and a successful login gives it back, so only
|
||||
failed or abandoned attempts drain it. At most 4 exchanges per address may be
|
||||
unfinished: an HTTP challenge never answered holds its slot for up to 30 s, a
|
||||
@@ -595,8 +634,8 @@ TCP connection that ends mid-exchange frees it at once. A refused start answers
|
||||
65,536 addresses and refuses new ones when full. Separately, at most 4,096
|
||||
exchanges may be in flight per listener; beyond that, or while the plugin
|
||||
stops, a login gets `busy` (HTTP `503`) and counts `auth_busy`. Peers behind
|
||||
one NAT or one passthrough proxy share a bucket: there, one client can exhaust
|
||||
every other client's logins.
|
||||
one NAT, or one passthrough proxy that sends no PROXY header, share a bucket:
|
||||
there, one client can exhaust every other client's logins.
|
||||
|
||||
### Credentials file
|
||||
|
||||
@@ -646,9 +685,9 @@ different users.
|
||||
|
||||
- TLS must terminate at LogWisp, except on an `http` sink in proxy mode. Behind
|
||||
any other terminating proxy or load balancer every login fails by design;
|
||||
pass TLS through instead (TCP or SNI routing). All clients then share the
|
||||
proxy's address, and so one throttling budget: see
|
||||
[Behind nginx or another proxy](#behind-nginx-or-another-proxy).
|
||||
pass TLS through instead (TCP or SNI routing), with a
|
||||
[PROXY header](#proxy-protocol) so each client keeps its own throttling
|
||||
budget.
|
||||
- One process per HTTP address. Handshake state and the token key live in one
|
||||
instance, so behind a balancer the proof or the token can reach an instance
|
||||
that never saw the login.
|
||||
@@ -756,7 +795,8 @@ A proxy in front of LogWisp works at one of two layers:
|
||||
overwrite `X-Forwarded-For` with the real client: the rightmost hop that is
|
||||
not a proxy is the client either way.
|
||||
- **L4, passing TLS through to LogWisp's own TLS** (nginx `stream` with
|
||||
`ssl_preread`, HAProxy `mode tcp`): whatever needs TLS to end at LogWisp.
|
||||
`ssl_preread`, HAProxy `mode tcp`), with a [PROXY header](#proxy-protocol):
|
||||
whatever needs TLS to end at LogWisp.
|
||||
- Chain links (`tcp_chain`, `http_chain`).
|
||||
- `lw auth stream` viewers of a `tcp` sink.
|
||||
- An `http` sink that keeps its own TLS, and with it SCRAM channel binding
|
||||
@@ -767,7 +807,7 @@ A common shape serves both from port 443:
|
||||
```
|
||||
:443 nginx stream: ssl_preread routes by SNI, proxy_protocol on
|
||||
logs.example.org -> 127.0.0.1:8443 nginx http
|
||||
relay.example.org -> a LogWisp TLS listener, without PROXY (below)
|
||||
relay.example.org -> a LogWisp TLS listener, which reads the header (below)
|
||||
127.0.0.1:8443 nginx http: listens with proxy_protocol, ends TLS
|
||||
/logs/ -> 127.0.0.1:8081 LogWisp http sink, proxy mode
|
||||
```
|
||||
@@ -796,52 +836,40 @@ client or, when it is trusted (both on `127.0.0.1`), the hop to its left: one
|
||||
the client wrote, so the client picks its own throttling address. LogWisp
|
||||
ignores `X-Real-IP`.
|
||||
|
||||
**LogWisp's own TLS listeners take no PROXY header.** LogWisp cannot read the
|
||||
PROXY protocol yet; it is planned first in the ACL work
|
||||
([To Do, 1.2](todo.md#12-proxy-protocol-deferred-gap-of-scram-see-scram-auth-planmd)).
|
||||
A stream route that sends `proxy_protocol` straight to a LogWisp TLS listener
|
||||
breaks every connection: the header lands in front of the TLS handshake. nginx
|
||||
sets `proxy_protocol` per stream `server`, so route LogWisp SNIs without it:
|
||||
|
||||
- Route 1: LogWisp gets its own stream `server` and port, without
|
||||
`proxy_protocol`.
|
||||
- Route 2: port 443 sends LogWisp SNIs to an internal stream `server` that
|
||||
`listen`s with `proxy_protocol` and proxy_passes to LogWisp without it; that
|
||||
hop consumes the header.
|
||||
- Either way dialers must send the name: a `host` that is one, or
|
||||
`tls.server_name` (`--server-name` for `lw auth`); an IP literal sends no SNI.
|
||||
- With HAProxy, leave `send-proxy` off the LogWisp backends.
|
||||
**LogWisp's own TLS listeners read the header** when their `acl` sets
|
||||
`proxy_protocol` and lists the stream server in `proxy_from`; one stream
|
||||
`server` with `proxy_protocol on` then routes every name:
|
||||
|
||||
```nginx
|
||||
stream {
|
||||
map $ssl_preread_server_name $route {
|
||||
relay.example.org 127.0.0.1:10443; # route 2: the internal hop
|
||||
tail.example.org 127.0.0.1:10443;
|
||||
default 127.0.0.1:8443; # the http block, logs.example.org
|
||||
}
|
||||
map $ssl_preread_server_name $logwisp {
|
||||
relay.example.org 127.0.0.1:9001; # tcp_chain source
|
||||
tail.example.org 127.0.0.1:9002; # tcp sink, for lw auth stream
|
||||
default 127.0.0.1:8443; # the http block, logs.example.org
|
||||
}
|
||||
server {
|
||||
listen 443;
|
||||
ssl_preread on;
|
||||
proxy_protocol on; # on every route of this server
|
||||
proxy_protocol on;
|
||||
proxy_pass $route;
|
||||
}
|
||||
server { # route 1: its own port, no header
|
||||
listen 9443;
|
||||
ssl_preread on;
|
||||
proxy_pass $logwisp;
|
||||
}
|
||||
server { # route 2: consumes the header
|
||||
listen 127.0.0.1:10443 proxy_protocol;
|
||||
ssl_preread on;
|
||||
proxy_pass $logwisp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```toml
|
||||
[pipelines.plugin_sources.config.acl] # tcp_chain source; tcp sink alike
|
||||
proxy_protocol = "required"
|
||||
proxy_from = ["127.0.0.1"] # the stream server
|
||||
```
|
||||
|
||||
- HAProxy: `send-proxy` or `send-proxy-v2` on the LogWisp backends.
|
||||
- A listener without `proxy_protocol` behind such a route fails every
|
||||
connection: the header lands in front of its TLS handshake.
|
||||
- Dialers must send the name: a `host` that is one, or `tls.server_name`
|
||||
(`--server-name` for `lw auth`); an IP literal sends no SNI.
|
||||
- `test/acl-test.sh --auto` runs each listener kind behind a stub proxy
|
||||
sending a v1 or v2 header, and the `tcp` sink behind nginx when installed.
|
||||
|
||||
**Passthrough without PROXY** brings every peer from the proxy's address:
|
||||
|
||||
- All peers share one SCRAM [throttling](#throttling) budget: 10 failed or
|
||||
@@ -853,14 +881,8 @@ stream {
|
||||
(`too many attempts` or `busy`) keeps sending on its token to the server it
|
||||
pinned, retrying every few seconds, and holds its batches only once that
|
||||
token has expired; any other refusal ends the token at once.
|
||||
- Logs and sessions name the proxy, not the peer, and `acl` rules see only the
|
||||
proxy: allow it, not the clients.
|
||||
- Proxy mode is unaffected: it throttles on the forwarded client.
|
||||
- Until LogWisp reads PROXY, mitigate:
|
||||
- expose the chain ports directly, without the proxy, where you can;
|
||||
- keep the proxy hop on a trusted network, so only trusted clients share the
|
||||
budget;
|
||||
- or accept the shared budget.
|
||||
- Logs and sessions name the proxy, and `acl` rules see only the proxy: allow
|
||||
it, not the clients.
|
||||
|
||||
## What Each Layer Enforces
|
||||
|
||||
|
||||
+8
-5
@@ -179,7 +179,8 @@ Options, each as type and default:
|
||||
- `auth` (table): client authentication (`mtls` or `scram`); see
|
||||
[Security](security.md#the-auth-block).
|
||||
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
|
||||
before TLS; see [Security](security.md#the-acl-block).
|
||||
before TLS, and `proxy_protocol` with `proxy_from` for the client a PROXY
|
||||
header names; see [Security](security.md#the-acl-block).
|
||||
|
||||
**Behaviour**
|
||||
|
||||
@@ -251,8 +252,8 @@ buffer sizes, connection limit, write timeout, uptime, endpoint paths, and the
|
||||
**Statistics**: `dropped_writes`, `rejected_clients`, `auth`, `auth_allowed`,
|
||||
`auth_rejected`; under `scram` also `auth_users`, `auth_throttled`,
|
||||
`auth_busy`, `auth_binding_mismatch` and `auth_token_lifetime_ms`, and in
|
||||
proxy mode `auth_trusted_proxies`; with `acl` rules also `acl` and
|
||||
`acl_denied`.
|
||||
proxy mode `auth_trusted_proxies`; with `acl` rules or `proxy_from` also
|
||||
`acl`, `acl_denied` and `acl_proxy_headers`.
|
||||
|
||||
> Without an `auth` block both endpoints are unauthenticated, and the stream
|
||||
> response carries `Access-Control-Allow-Origin: *`, so any web origin can read
|
||||
@@ -306,7 +307,8 @@ Options, each as type and default:
|
||||
- `auth` (table): client authentication (`mtls` or `scram`); see
|
||||
[Security](security.md#the-auth-block).
|
||||
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
|
||||
before TLS; see [Security](security.md#the-acl-block).
|
||||
before TLS, and `proxy_protocol` with `proxy_from` for the client a PROXY
|
||||
header names; see [Security](security.md#the-acl-block).
|
||||
|
||||
**Behaviour**
|
||||
|
||||
@@ -333,7 +335,8 @@ Options, each as type and default:
|
||||
**Statistics**: `write_errors`, `dropped_writes`, `rejected_conns`,
|
||||
`tls_handshake_errors`, `auth`, `auth_allowed`, `auth_rejected`; under `scram`
|
||||
also `auth_users`, `auth_throttled`, `auth_busy` and `auth_binding_mismatch`;
|
||||
with `acl` rules also `acl` and `acl_denied`.
|
||||
with `acl` rules or `proxy_from` also `acl`, `acl_denied` and
|
||||
`acl_proxy_headers`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+11
-7
@@ -215,7 +215,8 @@ Options, each as type and default:
|
||||
- `auth` (table): peer authentication (`mtls` or `scram`) and node binding;
|
||||
see [Security](security.md#the-auth-block).
|
||||
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
|
||||
before TLS; see [Security](security.md#the-acl-block).
|
||||
before TLS, and `proxy_protocol` with `proxy_from` for the client a PROXY
|
||||
header names; see [Security](security.md#the-acl-block).
|
||||
|
||||
For passwords instead of certificates, the `auth` block takes `type = "scram"`
|
||||
and a `credentials_file`, and `client_auth` becomes optional; see
|
||||
@@ -240,8 +241,9 @@ and a `credentials_file`, and `client_auth` becomes optional; see
|
||||
leaves per-entry labels to `trust_node`, so a relay can forward other nodes'
|
||||
entries while proving its own identity.
|
||||
- Each accepted connection gets a session recording the remote address, node
|
||||
label, — under TLS — `tls` and `tls_peer_cn`, and — under auth —
|
||||
`auth_method` and `auth_identity`.
|
||||
label, — under TLS — `tls` and `tls_peer_cn`, — under auth —
|
||||
`auth_method` and `auth_identity`, and behind a PROXY header the proxy as
|
||||
`peer_addr`.
|
||||
- A malformed entry line increments `parse_errors` and is skipped; the
|
||||
connection survives. A line over 1 MiB is a protocol violation and terminates
|
||||
the connection.
|
||||
@@ -249,7 +251,8 @@ and a `credentials_file`, and `client_auth` becomes optional; see
|
||||
**Statistics**: `active_connections`, `rejected_conns`, `parse_errors`,
|
||||
`tls_handshake_errors`, `trust_node`, `auth`, `auth_allowed`, `auth_rejected`,
|
||||
`node_binding`; under `scram` also `auth_users`, `auth_throttled`, `auth_busy`
|
||||
and `auth_binding_mismatch`; with `acl` rules also `acl` and `acl_denied`.
|
||||
and `auth_binding_mismatch`; with `acl` rules or `proxy_from` also `acl`,
|
||||
`acl_denied` and `acl_proxy_headers`.
|
||||
|
||||
---
|
||||
|
||||
@@ -301,7 +304,8 @@ Options, each as type and default:
|
||||
- `auth` (table): peer authentication (`mtls` or `scram`) and node binding;
|
||||
see [Security](security.md#the-auth-block).
|
||||
- `acl` (table): `allow` and `deny`, addresses or CIDRs admitted and refused
|
||||
before TLS; see [Security](security.md#the-acl-block).
|
||||
before TLS, and `proxy_protocol` with `proxy_from` for the client a PROXY
|
||||
header names; see [Security](security.md#the-acl-block).
|
||||
|
||||
**Behaviour**
|
||||
|
||||
@@ -328,8 +332,8 @@ Options, each as type and default:
|
||||
**Statistics**: `total_requests`, `rejected_requests`, `parse_errors`,
|
||||
`cached_sessions`, `trust_node`, `auth`, `auth_allowed`, `auth_rejected`,
|
||||
`node_binding`; under `scram` also `auth_users`, `auth_throttled`, `auth_busy`,
|
||||
`auth_binding_mismatch` and `auth_token_lifetime_ms`; with `acl` rules also
|
||||
`acl` and `acl_denied`.
|
||||
`auth_binding_mismatch` and `auth_token_lifetime_ms`; with `acl` rules or
|
||||
`proxy_from` also `acl`, `acl_denied` and `acl_proxy_headers`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+18
-76
@@ -7,98 +7,40 @@ they belong to.
|
||||
## 1. Network access control (ACL)
|
||||
|
||||
The seam exists: `internal/netacl` wraps each listener's socket before TLS,
|
||||
and the `acl` block's `allow` and `deny` refuse peers by address
|
||||
reads the PROXY header of the L4 proxies `proxy_from` lists, and the `acl`
|
||||
block's `allow` and `deny` refuse the client by address
|
||||
([Security](security.md#the-acl-block)). Every limit except SCRAM throttling
|
||||
is still global, and behind an L4 proxy every peer is the proxy.
|
||||
is still global.
|
||||
|
||||
### 1.1 The filtering listener
|
||||
|
||||
- `Accept` is to read an optional PROXY header (1.2) before the address rules
|
||||
and apply per-address connection caps (1.4) after them, returning a
|
||||
`net.Conn` whose `RemoteAddr` is the real client.
|
||||
- Everything downstream then sees the real client with no further change:
|
||||
`authz.remoteIP`, SCRAM throttling, sessions, logs and `http.Request.RemoteAddr`.
|
||||
- Keep the proxy's own address as `peer_addr` in session metadata for audit.
|
||||
|
||||
### 1.2 PROXY protocol (deferred gap of SCRAM, see scram-auth-plan.md)
|
||||
|
||||
- Why: behind an L4 proxy that passes TLS through (nginx `stream` with
|
||||
`ssl_preread`, HAProxy `mode tcp`), every peer arrives from the proxy's
|
||||
address.
|
||||
- All peers share one SCRAM throttling budget, so one client can lock out
|
||||
every login.
|
||||
- Logs, sessions and the ACL itself see only the proxy.
|
||||
- What: accept PROXY v1 (text) and v2 (binary) headers, but only from
|
||||
`proxy_from` addresses or CIDRs. Two config keys in the ACL block:
|
||||
- `proxy_protocol = "off" | "optional" | "required"`, default off.
|
||||
- `proxy_from = [...]`.
|
||||
- Rules:
|
||||
- A listed peer must send the header when `required`.
|
||||
- An unlisted peer that sends a header is refused, since the header is
|
||||
spoofable.
|
||||
- The header must arrive within a short deadline (reuse
|
||||
`tlsx.HandshakeTimeout`), with a size cap: v1 at most 107 bytes, v2 with a
|
||||
bounded TLV length.
|
||||
- `LOCAL`/`UNKNOWN` commands (proxy health checks) keep the socket address.
|
||||
- TLVs are ignored.
|
||||
- The header precedes the TLS ClientHello, so SCRAM channel binding is
|
||||
unaffected: LogWisp still terminates TLS.
|
||||
- nginx side:
|
||||
- In the `stream` server, `proxy_protocol on` on the route to LogWisp (it
|
||||
sends v1).
|
||||
- HAProxy: `send-proxy` or `send-proxy-v2`.
|
||||
- Relation to `auth.trusted_proxies` (HTTP proxy mode):
|
||||
- That list trusts `X-Forwarded-For` from a TLS-terminating L7 proxy;
|
||||
`proxy_from` trusts a PROXY header from an L4 proxy.
|
||||
- Keep both, with distinct names and docs. They answer different layers, and
|
||||
one deployment can use both: nginx `stream` with `proxy_protocol` into an
|
||||
nginx `http` block that sets `X-Forwarded-For $proxy_protocol_addr`.
|
||||
|
||||
### 1.3 Address rules for forwarded clients
|
||||
|
||||
- Rules match the real client (after PROXY), never the proxy.
|
||||
- In HTTP proxy mode, `X-Forwarded-For` is applied by `authz.ClientAddr` after
|
||||
the connection-level decision, so rules for forwarded HTTP clients belong in
|
||||
a second, request-level check there, reusing the compiled `netacl` rules.
|
||||
`authz.parseProxies` then takes netacl's address-or-CIDR parsing too.
|
||||
- Startup warning: `proxy_from` covering public ranges.
|
||||
|
||||
### 1.4 Per-client limits
|
||||
### 1.1 Per-client limits
|
||||
|
||||
- Generalize the SCRAM limiter in `internal/authz/scram.go` (token bucket per
|
||||
key, bounded table that fails closed, idle sweep, IPv6 per /64 via
|
||||
`throttleKey`) into the shared per-key limiter. One mechanism (AGENTS.md):
|
||||
SCRAM throttling then uses it too.
|
||||
- Keys:
|
||||
- Keys in the `acl` block:
|
||||
- `max_connections_per_client` on every listener (concurrent connections per
|
||||
address).
|
||||
client), applied by the netacl listener after the rules.
|
||||
- `requests_per_second_per_client` on HTTP listeners (ingest, stream and
|
||||
status requests).
|
||||
- A full table refuses new clients and counts them in `acl_limited`, beside
|
||||
`acl_denied` and `acl_proxy_headers`.
|
||||
- Later: per-identity limits once a peer is authenticated (the mtls plan's
|
||||
deferred item 2), keyed by identity instead of address.
|
||||
|
||||
### 1.5 Config, validation, docs
|
||||
### 1.2 Address rules for forwarded clients
|
||||
|
||||
- `proxy_protocol`, `proxy_from` and the 1.4 keys join `allow` and `deny` in
|
||||
the `acl` block.
|
||||
- Intent rule as in `authz`: `proxy_from` without `proxy_protocol` is an error.
|
||||
- Stats: `acl_proxy_headers` and `acl_limited` beside `acl_denied`.
|
||||
- Docs: the security.md section, plus networking.md troubleshooting lines for
|
||||
each refusal.
|
||||
- In HTTP proxy mode, `X-Forwarded-For` is applied by `authz.ClientAddr` after
|
||||
the connection-level decision, so rules for forwarded HTTP clients belong in
|
||||
a second, request-level check there, reusing the compiled `netacl` rules.
|
||||
`authz.parseProxies` then takes netacl's address-or-CIDR parsing too.
|
||||
|
||||
### 1.6 Verification
|
||||
### 1.3 Verification
|
||||
|
||||
- Go tests, one per rule:
|
||||
- PROXY v1 and v2 parse, including malformed, oversized and slow headers.
|
||||
- A header from an unlisted peer refused; `required` without a header
|
||||
refused; `LOCAL` kept.
|
||||
- Per-client connection cap; the limiter table failing closed.
|
||||
- The real client reaching SCRAM throttling and session metadata.
|
||||
- `test/acl-test.sh`, which covers the address rules, adds:
|
||||
- A Go stub proxy that sends v1 and v2 headers in front of each listener
|
||||
type.
|
||||
- nginx `stream` with `proxy_protocol on` when nginx is installed.
|
||||
- Per-client SCRAM budgets behind one proxy address.
|
||||
- Go tests, one per rule: the per-client connection cap; the request rate; the
|
||||
limiter table failing closed; a forwarded client refused by the rules.
|
||||
- `test/acl-test.sh` adds per-client connection caps, behind its stub proxy
|
||||
too.
|
||||
|
||||
## 2. Packaging: AUR, FreeBSD ports, Debian
|
||||
|
||||
|
||||
Reference in new issue
Block a user