v0.21.0 scram password auth, cred and token management, browser tls-terminating proxy, viewer client
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
1 parent
f192c26fa2
commit
9ab8c356d2
60 files changed
+6791
-747
No files matched your search
+220
-81
@@ -1,11 +1,12 @@
|
||||
# Password Authentication (Argon2id-SCRAM)
|
||||
|
||||
**Status:** planned. Ships in two batches:
|
||||
|
||||
1. **Done:** `lixenwraith/auth` gained SCRAM channel binding, and the mTLS layer
|
||||
was hardened (see [mTLS Hardening](#mtls-hardening)).
|
||||
2. **Next:** the SCRAM integration described here, pinned to the tagged `auth`
|
||||
release that carries channel binding.
|
||||
**Status:** implemented, in three batches: `lixenwraith/auth` gained SCRAM
|
||||
channel binding and the mTLS layer was hardened (see
|
||||
[mTLS Hardening](#mtls-hardening)); then the SCRAM integration described here,
|
||||
on the `auth` release that carries channel binding; then browser logins behind
|
||||
a TLS-terminating proxy. Operator documentation lives in
|
||||
[Security](security.md#password-authentication-scram) and
|
||||
[`logwisp auth`](cli.md#logwisp-auth).
|
||||
|
||||
**Scope:** optional username/password authentication on every network plugin,
|
||||
next to the existing certificate method, without weakening what mTLS gives.
|
||||
@@ -24,8 +25,10 @@ next to the existing certificate method, without weakening what mTLS gives.
|
||||
|
||||
- Standard SASL SCRAM-SHA-256 interoperability. The `auth` exchange is
|
||||
logwisp-to-logwisp (or logwisp CLI) only.
|
||||
- Browser `EventSource` viewers under SCRAM: they cannot set `Authorization` or
|
||||
run Argon2. mTLS remains the browser path.
|
||||
- Browser viewers of a logwisp that terminates TLS itself: a browser cannot see
|
||||
the server certificate, so its proof cannot be channel-bound. Browsers log in
|
||||
through [proxy mode](#browser-viewers-behind-a-tls-terminating-proxy), or use
|
||||
mTLS.
|
||||
- Multiple backends behind a non-sticky L7 balancer: handshake state and the
|
||||
token key live in one process.
|
||||
|
||||
@@ -35,23 +38,27 @@ next to the existing certificate method, without weakening what mTLS gives.
|
||||
`StoredKey = SHA-256(HMAC(K, "Client Key"))`, `ServerKey = HMAC(K, "Server Key")`,
|
||||
`K = Argon2id(password, salt)`. The Argon2id digest itself is never stored: it is
|
||||
the salted password, from which both keys derive. Verifying a proof is HMAC only;
|
||||
Argon2 runs on the dialer or CLI (64 MiB, t=3, the `auth` defaults).
|
||||
Argon2 runs on the dialer or CLI (t=3, 64 MiB, 4 threads, the `auth` defaults).
|
||||
The dialer refuses a challenge below that cost, so a rogue server cannot ask for
|
||||
a cheaply guessable proof, and trusts the link only after the server's final
|
||||
signature proves it holds the user's `ServerKey`.
|
||||
|
||||
**Channel binding.** Both peers pass `auth.WithChannelBinding(SHA-256(server leaf
|
||||
DER))` at the proof step. The server hashes its configured certificate; the
|
||||
client hashes the certificate it saw. A relay presenting any other certificate,
|
||||
even one the CA trusts, makes the server reject the proof, so it never obtains a
|
||||
link or a token. A mismatch is indistinguishable from a wrong password by design;
|
||||
the client also sends its hash as a diagnostic field so the listener can log
|
||||
"TLS interception or terminating proxy" and count it. TLS must terminate at
|
||||
logwisp.
|
||||
link or a token. A mismatch is indistinguishable from a wrong password on the
|
||||
wire by design; the client also sends its hash as a diagnostic field, so the
|
||||
listener logs "TLS interception or a terminating proxy" and counts it. TLS must
|
||||
terminate at logwisp, except in [proxy mode](#browser-viewers-behind-a-tls-terminating-proxy).
|
||||
|
||||
**HTTP pinning.** HTTP auth takes two requests and every later request may use a
|
||||
new pooled connection, so binding the exchange is not enough. After step 1 the
|
||||
dialer pins the bound hash on its `VerifyConnection`: step 2 and every ingest
|
||||
request must present the same leaf. A mismatch clears token and pin and
|
||||
re-authenticates, which also covers certificate rotation. A token handed to curl
|
||||
by the CLI is unpinned.
|
||||
new pooled connection, so binding the exchange is not enough. Once the challenge
|
||||
arrives the dialer pins the certificate that answered on its `VerifyConnection`:
|
||||
the proof and every ingest request must present the same leaf, or the handshake
|
||||
fails before anything is sent. The `http_chain` sink then clears token and pin
|
||||
and logs in again, which also covers certificate rotation. A token handed to
|
||||
curl by the CLI is unpinned.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -72,86 +79,107 @@ password_file = "/etc/logwisp/edge-01.pass"
|
||||
|
||||
Validation at construction:
|
||||
|
||||
- `scram` requires TLS; a dialer also forbids `insecure_skip_verify`.
|
||||
- Listeners require `credentials_file` and reject `allow`/`allow_patterns`: the
|
||||
credentials file is the allow list. Use one file per listener when listeners
|
||||
admit different users.
|
||||
- Dialers require `username` + `password_file`, and reject `credentials_file`,
|
||||
`allow*` and `identity` (server pinning stays an mTLS feature).
|
||||
- `scram` requires TLS, except on an `http` sink in proxy mode; a dialer also
|
||||
forbids `insecure_skip_verify`.
|
||||
- Listeners require `credentials_file` and reject `allow`/`allow_patterns` (the
|
||||
credentials file is the allow list), `username` and `password_file`. Use one
|
||||
file per listener when listeners admit different users.
|
||||
- Dialers require `username` + `password_file` (1–1024 bytes after trimming one
|
||||
line break), and reject `credentials_file`, `token_lifetime_ms`, `allow*` and
|
||||
`identity` (server pinning stays an mTLS feature).
|
||||
- `identity` on a listener requires `tls.client_auth` and **binds the certificate
|
||||
to the user**: the certificate's identity field must equal the SCRAM username,
|
||||
so a peer needs its own certificate and its own password (PostgreSQL's
|
||||
`clientcert=verify-full`). Without it the factors are independent: any
|
||||
CA-issued certificate plus any valid password (`clientcert=verify-ca`).
|
||||
- `token_lifetime_ms` only on HTTP listeners; default 15 minutes.
|
||||
- `/auth` is reserved; no configured path may equal it.
|
||||
- `/auth` and every path under it are reserved.
|
||||
- `mtls` rejects the four SCRAM keys. A block that names peers or credentials
|
||||
with `type = "none"` is refused: the type was forgotten, not the block.
|
||||
|
||||
**Credentials file** (TOML, written by the CLI, mode 0600):
|
||||
|
||||
```toml
|
||||
decoy_key = "<base64, 32 random bytes>"
|
||||
[[users]]
|
||||
username = "edge-01"
|
||||
salt = "<base64>"
|
||||
argon_time = 3
|
||||
argon_memory = 65536
|
||||
argon_threads = 4
|
||||
stored_key = "<base64>"
|
||||
argon_time = 3
|
||||
salt = "<base64>"
|
||||
server_key = "<base64>"
|
||||
stored_key = "<base64>"
|
||||
username = "edge-01"
|
||||
```
|
||||
|
||||
`decoy_key` keeps unknown-user challenges stable across restarts and edits, so
|
||||
probing usernames reveals nothing. It is created by the first `add-user` and
|
||||
preserved by every rewrite. An empty user set, a duplicate user, a missing
|
||||
`decoy_key` or mixed KDF profiles are configuration errors. The file is read at
|
||||
construction; the SCRAM server starts in the plugin's `Start` and stops in `Stop`,
|
||||
so a rejected reload leaks nothing.
|
||||
a single probe cannot tell a real user from an unknown one (a real user's salt
|
||||
changes when it is added, rotated or removed). It is created by the first `add-user` and
|
||||
preserved by every rewrite. An empty user set, a duplicate user, a missing or
|
||||
short `decoy_key`, an unknown key, or users differing in KDF profile or salt
|
||||
length are configuration errors. The file is read at construction; the SCRAM
|
||||
server starts in the plugin's `Start` and stops in `Stop`, so a rejected reload
|
||||
leaks nothing.
|
||||
|
||||
## Wire Protocol
|
||||
|
||||
`chain.Hello` gains `scram` (raw JSON, decoded by `authz`); the protocol version
|
||||
stays 1 because the field is additive. Every message after the hello is one
|
||||
`authStep`: `error`, `challenge`, `proof` (+ `binding`), `final` (+ `token`,
|
||||
`expires_in` on HTTP). Pre-auth lines are capped at 4 KiB.
|
||||
`expires_in` on HTTP). Pre-auth lines and `/auth` bodies are capped at 4 KiB.
|
||||
|
||||
**TCP** (`tcp_chain`, `tcp` sink): hello carrying the client-first message,
|
||||
challenge, proof, final — all under one deadline (`hello_timeout_ms` on the
|
||||
`tcp_chain` source, 10 s on the `tcp` sink and every dialer, with the dialer's
|
||||
context able to cut it short). The final is written only after every admission
|
||||
check (certificate binding, node binding); the stream then continues on the same
|
||||
buffered reader. A source without SCRAM answers a hello carrying credentials with
|
||||
`error: authentication not enabled`; only an old binary stays silent, and the
|
||||
dialer's deadline covers that.
|
||||
check (certificate binding, node binding); a later refusal is an `error` line
|
||||
instead. The stream then continues on the same buffered reader. A listener
|
||||
without SCRAM that reads a hello (the `tcp_chain` source) answers one carrying
|
||||
credentials with `error: authentication not enabled`; an old binary stays
|
||||
silent, and the dialer gives up after its deadline with "no challenge within
|
||||
10s".
|
||||
|
||||
**HTTP** (`http_chain` source, `http` sink): `POST /auth`, outside the auth
|
||||
middleware, with its own 10 s read/write deadline and a 4 KiB body limit.
|
||||
middleware, with its own 10 s read/write deadline. It answers `404` unless the
|
||||
policy is `scram`.
|
||||
|
||||
| Step | Request body | Success | Failure |
|
||||
|------|--------------|---------|---------|
|
||||
| 1 | hello | `200` challenge | `429` throttled, `503` handshake table full, `400` malformed |
|
||||
| 2 | proof | `200` final, token, expires_in | `401 {"error":"authentication failed"}` |
|
||||
| 1 | hello | `200` challenge | `429` throttled, `503` busy, `400` malformed, `413` oversized |
|
||||
| 2 | proof | `200` final, token, expires_in | `401 {"error":"authentication failed"}`, `503` if the server stopped |
|
||||
|
||||
Protected endpoints take `Authorization: Bearer <token>`: missing or invalid is
|
||||
`401` with `WWW-Authenticate: Bearer realm="logwisp"`, a certificate-binding miss
|
||||
is `403`. Tokens are HS256 JWTs from `auth.NewJWT` with a random per-instance key,
|
||||
so every reload revokes them. An SSE stream is checked at connect and outlives its
|
||||
token; a reload ends it.
|
||||
is `403`. Tokens are HS256 JWTs from `auth.NewJWT` (issuer `logwisp`, no leeway)
|
||||
with a random per-instance key, so every reload revokes them. An SSE stream is
|
||||
checked at connect and outlives its token; a reload ends it.
|
||||
|
||||
**Dialer outcomes** (`http_chain` sink): every `/auth` failure is transient (the
|
||||
batch is held under backoff); `404`/`405` from `/auth` logs "no auth endpoint:
|
||||
older logwisp or auth.type is not scram"; an ingest `401` clears the token and is
|
||||
retried; `403` drops the batch; a pin mismatch clears token and pin. Rejections
|
||||
are logged at WARN once per change of state and counted in `auth_failures` /
|
||||
`last_auth_error`.
|
||||
batch is held under backoff); `404`/`405` from `/auth` reads "no auth endpoint
|
||||
at <url> (older logwisp, or not a scram listener)"; an ingest `401` clears the
|
||||
token and is retried (tokens are renewed ahead of expiry, so this follows a
|
||||
listener reload); `403` drops
|
||||
the batch; a pin mismatch clears token and pin. Refusals are logged at WARN on
|
||||
every attempt, paced by the backoff (`Chain connect refused` on `tcp_chain`,
|
||||
`Chain batch delivery failed` on `http_chain`), and failed logins are counted
|
||||
in `auth_failures` / `last_auth_error`.
|
||||
|
||||
## Throttling
|
||||
|
||||
Per remote IP (the socket address, never a forwarded header), on handshake
|
||||
Per remote IP (the socket address; in proxy mode the forwarded client, an IPv6
|
||||
one per /64), on handshake
|
||||
starts: a token bucket (burst 10, 1/s) refunded on success, and at most 4
|
||||
unfinished exchanges. The table is bounded and fails closed when full. Abandoned
|
||||
TCP exchanges release their slot in the `auth` handshake table immediately.
|
||||
Counters: `auth_allowed` and `auth_rejected` once per exchange outcome;
|
||||
`auth_throttled`, `auth_busy` and `auth_binding_mismatch` separately.
|
||||
unfinished exchanges. An unanswered HTTP challenge holds its slot for the
|
||||
`auth` handshake timeout (30 s); abandoned TCP exchanges release their slot,
|
||||
and their entry in the `auth` handshake table, immediately. The address table
|
||||
holds 65,536 entries, drops one idle for a minute once its challenges have
|
||||
expired, and fails closed when full; the SCRAM server itself caps in-flight
|
||||
handshakes at 4,096 (`busy`).
|
||||
|
||||
Counters: `auth_allowed` counts logins; `auth_rejected` every refusal — failed
|
||||
proofs, malformed requests, missing credentials and, on HTTP, refused tokens;
|
||||
`auth_binding_mismatch` is the subset of failed proofs whose client saw another
|
||||
certificate. `auth_throttled` and `auth_busy` count starts refused before a
|
||||
challenge and are not in `auth_rejected`.
|
||||
|
||||
## The `authz` Seam
|
||||
|
||||
@@ -161,11 +189,13 @@ closed.
|
||||
|
||||
- `Admit(conn, cs, wantHello, timeout)` returns an admission (identity, hello,
|
||||
reader) whose `Accept` or `Reject` writes the final message.
|
||||
- `AuthorizeRequest(r)` covers mTLS certificates and bearer tokens;
|
||||
`ServeAuth(w, r)` serves `/auth`.
|
||||
- Dialers: `Greet(ctx, conn, node)` writes the hello (plain or SCRAM);
|
||||
`Authenticate`, `SetAuthorization`, `ClearToken` and `VerifyConnection` serve
|
||||
HTTP.
|
||||
- `AuthorizeRequest(r)` covers mTLS certificates and bearer tokens, returning
|
||||
the refusal status for `Refuse`; `ServeAuth(w, r)` serves `/auth`.
|
||||
- Dialers: `Greet(ctx, conn, node)` writes the hello and, under SCRAM, runs the
|
||||
TCP exchange. Over HTTP `Prepare` logs in when no token is held and sets the
|
||||
bearer header, `Invalidate` drops the token after a `401` or a pin mismatch,
|
||||
`Token` is the login itself (behind `Prepare`, and for the CLI), and
|
||||
`VerifyConnection` pins.
|
||||
- `Start`/`Close` bracket the SCRAM server and are nil-safe like everything else.
|
||||
|
||||
## CLI
|
||||
@@ -173,24 +203,27 @@ closed.
|
||||
```
|
||||
logwisp auth add-user -credentials F -user U [-password-file P] [-generate]
|
||||
logwisp auth remove-user -credentials F -user U
|
||||
logwisp auth token -url https://host:port -user U -password-file P [TLS flags]
|
||||
logwisp auth token -url https://host:port[/path] -user U -password-file P [-unbound] [TLS flags]
|
||||
logwisp auth stream -addr host:port -user U -password-file P [TLS flags]
|
||||
```
|
||||
|
||||
- `add-user` takes the password from `-password-file` when it exists; a new user
|
||||
without one gets a generated 130-bit password, written to that file (0600) or
|
||||
printed once. Rotating an existing user needs the file or `-generate`, so a
|
||||
mistyped path cannot replace a password. The file's KDF profile and decoy key
|
||||
are reused, the result is validated with the daemon's own loader, and the
|
||||
rewrite (temp file in the same directory, rename) keeps the existing mode and
|
||||
owner.
|
||||
- `add-user` takes the password from `-password-file` when it exists (at least
|
||||
8 bytes); a new user without one gets a generated 130-bit password, written to
|
||||
that file (0600) or printed once. Rotating an existing user needs the file or
|
||||
`-generate`, so a mistyped path cannot replace a password. The file's KDF
|
||||
profile and decoy key are reused, the result is validated with the daemon's
|
||||
own parser, and the rewrite (temp file beside the file or a symlink's target,
|
||||
rename) keeps the existing mode and owner, or fails.
|
||||
- `remove-user` refuses to remove the last user.
|
||||
- Both print that changes apply on `SIGHUP`: `auto_reload` does not watch the
|
||||
credentials file.
|
||||
- `token` prints a bearer token for curl (`-H @<(...)` keeps it out of argv);
|
||||
`stream` authenticates to a `tcp` sink and copies the stream to stdout.
|
||||
`stream` authenticates to a `tcp` sink and copies the stream to stdout until
|
||||
interrupted. `-url` takes a path only with `-unbound`, and redirects are not
|
||||
followed.
|
||||
- TLS flags: `-ca-file`, `-server-name`, `-cert-file`, `-key-file`. There is no
|
||||
insecure flag.
|
||||
- Exit status: 0 success, 1 failure, 2 usage error.
|
||||
|
||||
## Rollout and Rotation
|
||||
|
||||
@@ -202,18 +235,108 @@ logwisp auth stream -addr host:port -user U -password-file P [TLS flags]
|
||||
between.
|
||||
- Revocation: `remove-user`, `SIGHUP`. All tokens die with the reload.
|
||||
|
||||
## Browser Viewers Behind a TLS-Terminating Proxy
|
||||
|
||||
The deployer serves the log stream inside a website whose TLS ends at its
|
||||
reverse proxy; LogWisp holds no private key and listens on a trusted hop.
|
||||
Viewers log in from a browser with the client LogWisp ships, and the password
|
||||
reaches neither the proxy nor LogWisp. Operator view and example:
|
||||
[Security](security.md#browsers-behind-a-tls-terminating-proxy).
|
||||
|
||||
**Trust model.** `auth.trusted_proxies` (addresses or CIDRs) switches the `http`
|
||||
sink into proxy mode; it is refused on every other plugin and with `identity`.
|
||||
- Only listed peers may connect: `403` on every path, the pages included.
|
||||
- `X-Forwarded-Proto` must be `https` on every hop, so a site served in
|
||||
plaintext by mistake fails closed instead of exposing sessions.
|
||||
- The client is the rightmost `X-Forwarded-For` hop that is not a proxy (hops to
|
||||
its left are the client's own claims), or the leftmost when every hop is
|
||||
inside a proxy range; throttling (an IPv6 client per /64, as one host holds
|
||||
it whole), sessions and logs use it. Outside proxy mode forwarded headers
|
||||
stay ignored.
|
||||
- SCRAM runs unbound: a browser cannot read the site's certificate, and LogWisp
|
||||
sees none. The proof and the session ride on the site's TLS; a party on the
|
||||
proxy-to-LogWisp hop could relay a login, hence the address restriction and a
|
||||
startup warning for a plaintext hop to a proxy off this host. `tls` may stay
|
||||
off. Outside proxy mode nothing changes: proofs stay bound, and a bound proof
|
||||
in proxy mode fails with a hint naming `-unbound`.
|
||||
|
||||
**Session.** The proof step may ask for a cookie (`"session": "cookie"`, proxy
|
||||
mode only). The answer sets `logwisp_session` (`HttpOnly; Secure;
|
||||
SameSite=Strict; Max-Age=<lifetime>`) and carries no token in the body. It has
|
||||
no `Path`: RFC 6265 defaults it to the directory of `POST <mount>/auth`, which
|
||||
is the mount behind any proxy prefix. Stream and status accept the cookie or
|
||||
`Authorization: Bearer`; another `Authorization` scheme, such as the site's own
|
||||
Basic auth, leaves the cookie in charge. Logout is `{"logout": true}` on `POST /auth` itself,
|
||||
because a clearing cookie set from `/auth/logout` would default to another path
|
||||
and miss the login cookie. It clears the cookie and revokes a valid token until
|
||||
it expires (a set of at most 65,536 hashes, gone on reload like every token). An
|
||||
open stream outlives its token; a reconnect after expiry gets `401`. `/auth`
|
||||
takes only `application/json`, which a cross-origin page cannot send without a
|
||||
preflight nobody answers.
|
||||
|
||||
**Endpoints.** `/auth` and every path under it are reserved; every URL in the
|
||||
pages is relative, so a proxy prefix works unchanged.
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `POST /auth` | SCRAM hello, proof, or logout; unbound in proxy mode |
|
||||
| `GET /auth/scram.js` | the client library, always in proxy mode |
|
||||
| `GET /auth/login`, `login.js`, `style.css` | login page (`login_page`) |
|
||||
| `GET /auth/view`, `view.js` | minimal live viewer (`viewer_page`, needs `login_page`) |
|
||||
|
||||
Files carry `default-src 'none'; script-src 'self'; connect-src 'self';
|
||||
style-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`,
|
||||
`nosniff` and `no-referrer`; nothing is inline. The viewer learns a custom
|
||||
`status_path` from a meta tag the sink fills in, renders entries as text, and
|
||||
sends a signed-out visitor to `login?next=view`; the login page follows `next`
|
||||
only within its origin. The `proxy_tls` capability satisfies the pipeline's
|
||||
"auth needs TLS" check.
|
||||
|
||||
**Client library.** `internal/sink/http/web/scram.js`, embedded with `go:embed`,
|
||||
is one dependency-free ES module, so a site with its own CSP can copy it into
|
||||
its bundle. It exports `login(base, username, password, {onProgress})` and
|
||||
`logout(base)`. BLAKE2b and Argon2id are plain JS (no WebAssembly, which would
|
||||
need `'wasm-unsafe-eval'`), yielding to the page every 50 ms; SHA-256 and HMAC
|
||||
come from WebCrypto, so it needs a secure context. It checks the challenge as
|
||||
the Go client does (nonce, salt, cost limits and the dialer's Argon2 floor),
|
||||
refuses redirects, and reports success only after verifying the server's
|
||||
signature. A 64 MiB login takes about 2 s on a desktop. The wire protocol above
|
||||
is the contract: a site may implement its own client.
|
||||
|
||||
**CLI.** `logwisp auth token -unbound` logs in through the proxy, still pinning
|
||||
its certificate across the two requests; only then may `-url` carry the mount
|
||||
path.
|
||||
|
||||
## Verification
|
||||
|
||||
Go tests, one per rule: binding (match, mismatch, one-sided, length) in `auth`;
|
||||
in `authz` the validation table, credential loading, decoy stability across
|
||||
edits, TCP exchange over `net.Pipe` (success, wrong password equals unknown
|
||||
user, binding mismatch, missing hello, silent peer within the deadline,
|
||||
abandoned exchange frees its slot, final and first entry in one write, assert
|
||||
mismatch gets an error rather than a final), HTTP exchange on a TLS test server
|
||||
Go tests, one per rule: binding (match, mismatch, one-sided, length) in `auth`.
|
||||
In `authz`: the validation table (each row valid but for its rule, the error
|
||||
naming it) and credentials file validation; the TCP exchange over `net.Pipe`
|
||||
(admission with the first entry in the same read, wrong password
|
||||
indistinguishable from an unknown user, another certificate refused and counted,
|
||||
hello and policy disagreeing either way, a dialer cut short by its context, an
|
||||
abandoned exchange freeing its handshake and limiter slots, a refusal after the
|
||||
exchange sent as a reason rather than a final, certificate-to-user binding for
|
||||
logins and tokens); only failed attempts throttled, the limiter bounds
|
||||
(reservation under concurrency, the sweep of abandoned challenges); pre-auth
|
||||
input capped at 4 KiB; the decoy salt stable across restarts; a dialer refusing
|
||||
a challenge below its Argon2 floor; the HTTP exchange on a TLS test server
|
||||
(token accepted, garbage and foreign tokens `401`, forged final yields no token,
|
||||
a different certificate after step 1 refused before the body is sent), the
|
||||
limiter bound; `http_chain` sink re-authentication and outcome table; CLI
|
||||
add/remove/rotate, mode preservation, last-user refusal, password trimming.
|
||||
a different certificate after the challenge refused before the proof is sent,
|
||||
renewal ahead of expiry); `Authorize` failing closed under SCRAM; password
|
||||
trimming; refused hellos freeing their slot; unbound logins pinned too. Proxy
|
||||
mode: trusted peers, https and the client hop; throttling on the forwarded
|
||||
client and IPv6 /64; an unbound browser login with the cookie's attributes, the
|
||||
cookie opening endpoints (beside Basic auth) until logout revokes it, cookie
|
||||
sessions refused elsewhere; only unbound logins behind a proxy; `/auth` taking
|
||||
only JSON. In the `http` sink, every SSE line break framed as data.
|
||||
In the plugins: `/auth` outside the `http` sink's gate, the pages served with
|
||||
their types and CSP, direct peers refused, pages only in proxy mode, and the
|
||||
`http_chain` sink logging in again after a `401` and delivering once. In the
|
||||
CLI: private files with a matching verifier, no password replaced without a
|
||||
source, mode, decoy key and symlink kept across rewrites, an empty file filled,
|
||||
last-user refusal. Under node, `scram.js` against the RFC 9106 vector, Go's
|
||||
`argon2.IDKey`, and `auth`'s known-answer proof.
|
||||
|
||||
`test/scram-chain-test.sh --auto` (ports 15821-15825): a relay with SCRAM on all
|
||||
four listener types plus one `client_auth` + SCRAM port; authorized edges over
|
||||
@@ -222,7 +345,20 @@ user and no-auth edges deliver nothing; a `tcp` sink viewer through `logwisp aut
|
||||
stream` held past the exchange deadline; a raw TLS client without a hello gets
|
||||
nothing; the `http` sink through `logwisp auth token` and curl; missing and bad
|
||||
tokens get `401`; `remove-user` + `SIGHUP` revokes token and login; throttling
|
||||
last. The existing scripts keep passing.
|
||||
last. `test/scram-proxy-test.sh --auto` (ports 15831-15832): a Go reverse proxy
|
||||
ending TLS in front of a plaintext `http` sink mounted at `/logs/`; headless
|
||||
Chromium sent to the login page, refused a wrong password, streaming events
|
||||
after login, with the cookie scoped to `/logs` and hidden from scripts, cleared
|
||||
by sign-out, and no CSP violations; `token -unbound` and curl through the
|
||||
proxy; direct peers and plaintext-forwarded requests `403`. The existing
|
||||
scripts keep passing.
|
||||
|
||||
## Not Implemented
|
||||
|
||||
1. **PROXY protocol behind TLS passthrough.** Every client then shares the
|
||||
proxy's address, and so one throttling budget that a single client can
|
||||
exhaust. Accepting PROXY v2 from listed proxies would restore per-client
|
||||
throttling on the TCP listeners and on HTTP listeners outside proxy mode.
|
||||
|
||||
## mTLS Hardening
|
||||
|
||||
@@ -234,10 +370,13 @@ Shipped with this plan, independent of SCRAM:
|
||||
- The `tcp` sink tracks connections from accept, so `Stop` and reloads no longer
|
||||
wait out the 10 s handshake timeout of a silent peer.
|
||||
- Unknown keys in any plugin's config, at any depth, fail construction: a typo
|
||||
such as `[...tls] enabeld = true` used to switch protection off silently.
|
||||
such as `[...tls] enabeld = true` used to switch protection off silently. The
|
||||
configuration file itself is checked the same way, so a misspelled table path
|
||||
above a plugin's config fails too.
|
||||
- Startup and every reload warn about certificates that expired, are not yet
|
||||
valid, or expire within 30 days; about `insecure_skip_verify`; about
|
||||
`allow_patterns` that are not anchored at both ends of every alternative; and,
|
||||
once per path, about key files every local user can read.
|
||||
once per path, about key, credentials and password files every local user can
|
||||
read.
|
||||
- An `http` sink with an auth policy no longer sends
|
||||
`Access-Control-Allow-Origin: *`.
|
||||
Reference in new issue
Block a user