P0 #8 — install.sh fetched and installed the binary with no integrity
check whatsoever, despite README claiming cosign verification. A
compromised release or registry MITM resulted in RCE on every
installer.
Now:
- downloads checksums.txt alongside the archive (required; abort on
missing)
- computes local SHA-256 with shasum -a 256 (works on macOS+Linux,
not GNU-only sha256sum)
- aborts on mismatch with a clear error
- if cosign is in PATH AND the sigstore bundle is present (the latter
already published by goreleaser), verifies cert-identity. Skipped
silently when cosign is absent so the install path still works for
users without cosign installed.
- SKIP_COSIGN=1 lets users opt out of cosign verification only
(checksum verification is always enforced).
- DRY_RUN=1 verifies + downloads but does not install, for testing.
Also replaced GNU-only `grep -oP` (silently fails on macOS BSD grep)
with portable awk for parsing kportal --version.
NOTE: the cosign cert-identity regex matches lukaszraczylo/kportal/.*
but actual releases are signed from the shared-actions reusable
workflow. Users with cosign installed will currently see a verification
failure on real releases. Either widen the regex to lukaszraczylo/.*
or change the signing identity scheme — flagging for follow-up.
README install section updated to mention the new verification.
P0 #1 — HTTP traffic logger captured Authorization, Cookie, Set-Cookie,
X-Api-Key, X-Auth-Token, X-Csrf-Token, Proxy-Authorization, X-Access-Token
verbatim into log entries (file 0600 + UI subscribers). Bearer tokens
and session cookies were ending up on disk whenever httpLog.includeHeaders
was enabled.
flattenHeaders now redacts:
- the explicit list above (case-insensitive via http.CanonicalHeaderKey)
- any header name containing 'token', 'secret', 'password', 'apikey'
Header names remain visible; values become [REDACTED].
Redaction is unconditional and on-by-default — no opt-out flag. Users
who want raw headers can use tcpdump.
P0 #6 — Headless mode without -v silently routed both structured and
stdlib logs to io.Discard. A daemon under launchd/systemd had no way to
report errors. Headless now defaults log destination to os.Stderr; -v
controls only the level (debug vs info). TUI-quiet path is preserved.
Tests in internal/httplog/redact_test.go cover all explicit names,
substring patterns, and case variants.
In the remove-wizard's confirming state, pressing Esc was reflexively
calling removeForwardsCmd — i.e. confirming deletion. The on-screen
help text said 'Esc: Cancel'. Reflexive Esc-to-cancel destroyed data.
Esc now sets confirming=false and resets the cursor; deletion
requires Enter on Yes. Non-confirming Esc behavior (exit wizard with
ClearScreen) is unchanged.
Three regression tests added in handlers_test.go.
P0 #2 — currentConfig data race
Manager.currentConfig was written without locking in Start/Reload but
read from the health-checker callback goroutine. All accesses now go
through workersMu (read or write as appropriate).
P0 #3 — Reload kills health checker permanently
Reload's zero-forward branch called m.Stop() which tore down the
health checker, watchdog, and event bus. After that, EnableForward
silently registered callbacks against dead components. Now the branch
stops only the running workers; the supervisory infrastructure stays
alive across config changes.
P0 #4 — rest.Config write-write race
executePortForward was mutating .Dial on the cached *rest.Config
shared by all forwards in the same kube context. Cloning the config
with rest.CopyConfig before mutation isolates per-forward dialers.
P0 #5 — ForwardWorker.Stop() double-close panic
close(w.stopChan) is now wrapped in sync.Once, so concurrent Stop
calls (Manager.Stop racing stopWorkerInternal) are safe.
New tests in internal/forward/concurrency_test.go exercise each fix
under -race: 16 concurrent worker Stops, repeated sequential Stops,
empty-Reload preserves infra pointers, and concurrent currentConfig
read/write.
CI runners have no kubeconfig, so clientcmd's loader returns an empty
config (no error) and CurrentContext == "". The previous assertion
'NotEmpty(context)' on the success branch was incorrect — an empty
current-context is valid for an empty kubeconfig.
Mirrors the looser pattern in TestDiscovery_ListContexts.
- [x] Add golangci-lint v2 configuration with formatters section
- [x] Reorganize linters-settings under linters section
- [x] Replace if-else chains with switch statements for clarity
- [x] Wrap all ignored error returns with `_ = ` pattern
- [x] Add OSC 8 hyperlink helper function for clickable ports
- [x] Add blank line in table styling function
- [x] Remove unnecessary type assertion in test