`sowerd -i` wrote config.ExampleSowerdConfigTOML verbatim, so a fresh
install started with `password = "change_me"` — a publicly known credential
— while Validate rejects only an empty password. The admin console already
generates a random password when none is configured; do the same here.
- renderDefaultConfig replaces the template's top-level `password = ` line
with a random 128-bit hex value (the commented `# password = ...` sample
inside site_routes.auth is left alone) and errors when the template has no
such line, instead of silently keeping a known value.
- The password is printed once during install for the client configs, and an
existing /etc/sower/sowerd.toml is still never rewritten.
- docs: security.md states this under "What the Transport Password Gates"
and moves the risk out of Residual Accepted Signals — a fixable
operational defect is not an inherent signal — recording the declined
alternative (a placeholder blocklist in Validate) with its rationale;
ARCHITECTURE/README describe the generated password.
Gate: gofmt, go vet ./..., go test ./... green; install_e2e_test PASS.
Site-route domains become public once autocert issues for them
(Certificate Transparency), and the edge answers any SNI. An upstream that
cannot authenticate its own visitors is therefore exposed, and sowerd owns
80/443 on the host, so nothing can be placed in front of it.
Add an opt-in per-route visitor gate:
- config: site_routes[].auth {username, password}; both fields required,
empty means ungated, ':' rejected in the username (RFC 7617 splits there).
- runtime: the gate runs in the reverse-proxy handler, so it covers path
overrides and WebSocket upgrades too; the sower transport path is
dispatched earlier and stays untouched. Credentials are compared in
constant time, failed attempts accumulate per source IP and lock it out for
15 minutes (bounded map, mirroring the admin login throttle), and a passing
request has its Authorization header removed so the edge credential never
reaches the upstream. The fake-site decoy is deliberately never gated.
- fix: upstream URL userinfo was silently dropped. ReverseProxy sends
through a Transport, and only net/http.Client turns userinfo into Basic
credentials, so sowerd now sets that header itself; the upstream password
is redacted in proxy error logs (url.URL.Redacted).
- docs: security.md records the mechanism and its limits (not a substitute
for upstream auth, no session/logout, CORS preflight blind spot, and the
browser WebSocket handshake: script cannot set Authorization, cached-
credential replay is browser-specific and unverified, and a reconnecting
client's failed handshakes feed the same per-IP counter, so it can lock its
own source IP out of the whole route) plus the rejected alternatives
(session login, gating the fake site).
Gate: gofmt, go vet ./..., go test ./... green; install_e2e_test PASS.
Installing sower meant grabbing a tarball and wiring the service up by hand,
and the packaged unit was too fragile for something a resolver depends on
(on-failure restart after 1min, no memory limit).
Add install.sh: detects the platform and init system, installs or upgrades
the binary atomically, keeps an existing config, verifies the release
SHA256SUMS unless explicitly opted out, and registers the service on systemd,
OpenWrt procd or macOS launchd. Every network call carries an explicit
timeout, the OpenWrt init script ships in Linux tarballs, the release
workflow generates SHA256SUMS, the README documents the installer, and
make test syntax-checks the scripts.
deferlog v2.1.5 removes the base64-or-plain heuristic from Password input:
values are verbatim unless explicitly prefixed with "b64:", and marshaling
emits the same tagged form for exact round-trips. The admin password keeps
deferlog.Password and now takes "b64:"-prefixed encoded values; plaintext
passwords (even ones that are valid canonical base64) are never decoded.
Update the tagged-base64 test and document the prefix in README.
Review follow-up: the load-failure path logged the whole partially-populated
SowerConfig, which re-exposed the plain Remote.Password string after the
verbatim-password fix. Extract loadConfig/logConfigLoadError so the error
log carries only the error and remote_addr, matching sowerd. Add tests
pinning the redaction, JSON-handler redaction for the admin password, and
verbatim loading of sowerd's password. Document the no-base64 semantics of
remote/sowerd passwords and the admin password base64 caveat in config
comments and README.
ARCHITECTURE.md and README.md now describe editable config fields,
in-place restart, rule hit/miss stats, and session persistence; the
admin console plan is marked implemented.
- internal/admin: session-cookie auth server, rule CRUD API, bounded
per-domain traffic stats with pre-parse byte accounting
- cmd/sower: wrap proxied connections and DNS queries into stats, serve the
admin console on its own listener
- web/: Svelte 5 + Vite admin console with shadcn-svelte UI and an advanced
breadcrumb header (section/context dropdowns, favorites), embedded via
go:embed and committed to web/dist
- Makefile: sower build depends on the frontend build (bun); sowerd does not
- docs: ARCHITECTURE, README, and plan updated for the admin surface