Skip to content

Security hardening, 2026 feature set, public-repo restructure (v0.9.0)#5

Merged
Booyaka101 merged 16 commits into
mainfrom
security-hardening-phase0
Jun 10, 2026
Merged

Security hardening, 2026 feature set, public-repo restructure (v0.9.0)#5
Booyaka101 merged 16 commits into
mainfrom
security-hardening-phase0

Conversation

@Booyaka101

Copy link
Copy Markdown
Owner

Brings porter to a clean, public-ready v0.9.0.

Security

  • Verified SSH host keys everywhere (TOFU/strict + TrustHostCA step-ca host certs); SSH-certificate auth (ConnectWithCert), bastion/ProxyJump (ConnectViaJump), keepalives.
  • Sudo passwords fed via stdin (never the process table); sshpass via SSHPASS env.
  • Dashboard auth wiring fixed and secure-by-default (PORTER_AUTH=0 to disable); random first-boot admin password; agent-channel shared-secret auth; WS/SSE origin-checked; AES credential storage fails closed.

2026 feature set

  • Declarative Ensure* primitives (fact-gather → diff → no-op) + a real --dry-run preview; fixed Changed accounting.
  • Goss-style Assert* health gates.
  • Atomic releases with health-gated cutover and rollback (NewRelease, Rollback).
  • Deploy-as-an-OpenTelemetry-trace (NewTracer) + slog; dashboard /traces waterfall.
  • Secrets: SOPS+age (Secret) and pluggable CLI backends (SecretCommand).
  • Supply-chain gate: cosign VerifyBlob/VerifyImage.

Repo / code quality

  • Library is the front door; the optional dashboard moved to web/ + cmd/porter-ui.
  • Action dispatch refactored from a 740-line switch into a handler registry (actions_*.go).
  • Removed committed binaries/built assets; added CONTRIBUTING/SECURITY/CODE_OF_CONDUCT/.golangci.yml, docs/architecture.md; fixed a fresh-clone go:embed build break.
  • Per-domain 2026 command audit: safe fixes (apt DEBIAN_FRONTEND, quiet tar, curl TLS hardening, errors.As); gopls modernize; whole module is staticcheck-clean.

Build, vet, go test -race, gofmt, and staticcheck all green.

🤖 Generated with Claude Code

CBosch101 and others added 16 commits June 9, 2026 12:36
… atomic releases, OTel, secrets, cosign)

Implements the full backlog from PORTER_IMPROVEMENT_PLAN.md against 2026-current
research. Zero new Go module dependencies (external-tool features shell out to
sops/cosign by design).

Security (Phase 0):
- hostkey.go: verified host keys across all SSH/scp/rsync paths — TOFU (pins on
  first use, rejects changed keys as MITM) / strict / insecure modes, plus
  TrustHostCA for step-ca host certificates. Drops every InsecureIgnoreHostKey
  and StrictHostKeyChecking=no.
- Sudo password fed via `printf '%s\n' '<quoted>' | sudo -S -p ''` (never echo,
  never the process table, quoted against injection) — core executor + all 25
  porterui sites via porterui/shell.go (sudoStdin/shellQuote). sshpass moved to
  SSHPASS env.
- porterui auth wiring fixed: SetupRoutesWithAuth now actually enforces
  (r.Use(AuthMiddleware) + self-filtering public/agent allowlist), gated by
  PORTER_AUTH. React auth fails closed. AES crypto fails closed. admin/admin
  replaced by random/PORTER_ADMIN_PASSWORD. WS CheckOrigin same-origin
  (porterui/wsorigin.go). scp fallback -> accept-new.

Features (Phases 1-3):
- ensure.go: EnsureFile/Dir/Symlink/Package/Line/ServiceRunning/ServiceEnabled
  fact-gather -> diff -> no-op, with a real --dry-run preview.
- executor.go: Changed stat now meaningful (exec returns changed; read-only and
  converged tasks report ok-not-changed).
- release.go: atomic releases + health-gated rename(2) symlink swap + Rollback,
  for systemd/VM targets.
- tracer.go: deploy-as-an-OTel-trace (JSONL spans, stable deployment.environment.name)
  + slog structured logs with trace_id correlation.
- secrets.go: Secret() decrypts via SOPS+age locally, ships 0600 over SFTP, never
  logged.
- verify.go: VerifyBlob/VerifyImage cosign pre-deploy admission gate.
- sshcert.go: ConnectWithCert (step-ca/Vault SSH user certs) + StartKeepalive;
  Config.Port.

Tests: hostkey_test, features_test, porterui/auth_middleware_test,
porterui/security_test. Build/vet/test all green. Worked example in
examples/modern/main.go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agent-token auth, SSE/CORS + dir-transfer hardening

- assert.go: Goss-style health assertions (AssertServiceActive/Enabled,
  Process, PortListening, FileExists/Contains, Package, HTTPStatus, Command) —
  read-only fail-closed gates for post-deploy smoke tests / pre-flight guards.
- SecretCommand(fetchCmd, dest): pluggable secret backend for Vault / OpenBao /
  1Password / Infisical via their CLIs; same 0600/never-logged guarantees as
  the SOPS Secret().
- PORTER_AGENT_TOKEN: optional shared-secret auth on the agent / standalone-agent
  / build-client channels (constant-time compare, header or query) so they can
  be locked down independently of human PORTER_AUTH; open when unset.
- SSE endpoints (logs.go, streaming.go): wildcard Access-Control-Allow-Origin:*
  replaced with same-origin/allowlisted allowSSEOrigin (CSRF on log/output
  streams).
- UploadDir/DownloadDir (connection.go): shell-quote all paths; unpredictable
  remote temp names via randomID (no /tmp race/symlink attack) instead of
  time.Now().UnixNano().

PQ-SSH (mlkem768x25519-sha256) confirmed already negotiated by default in the
installed x/crypto — no change needed. Build/vet clean; 154 test assertions
green (assert/SecretCommand builders, agentTokenValid, agent-channel gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nded idempotent primitives

- bastion.go: ConnectViaJump(target, jumps...) — ProxyJump through one or more
  bastions with host-key verification at every hop and no agent forwarding
  (2026 best practice). Hop + PasswordAuth/KeyAuth/AgentAuth helpers.
- PORTER_AUTH now secure-by-default: JWT enforced unless explicitly disabled
  with PORTER_AUTH=0/off/false/no. First-boot random admin password covers the
  default; agent channels still gated separately by PORTER_AGENT_TOKEN.
- porterui deploy tracing: manifest deploys now record an OTel-shaped JSONL
  trace under <dataDir>/traces/ + structured slog (manifest.go, deploytrace.go).
- ensure.go: EnsureCron (fixes cron_add duplicate-append + injection),
  EnsureUser (fixes non-idempotent useradd), EnsureMode, EnsureOwner,
  EnsureAbsent, EnsureGitRepo (clone-or-fast-forward) — all idempotent with
  --dry-run previews.

159 test assertions green; build/vet/gofmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…waterfall viewer

- cmdRunner seam: Executor command execution now goes through a small interface
  (*goph.Client satisfies it; NewExecutor wires runner: client). No behavior
  change, but runtime_test.go can now drive the dispatch switch with a fake —
  closing the audit's "entire runtime untested" gap. Covers ensure_* no-op
  detection, writes-when-different, service start, idempotent cron, assertions
  (fail-closed), retry attempts, and Changed accounting (read-only vs mutating).
- Trace viewer (porterui/traces.go): GET /api/traces (list), GET
  /api/traces/{name} (parsed spans, path-traversal-guarded), and a
  self-contained waterfall UI at /traces. Renders the JSONL traces the manifest
  deploy path already writes. Tested in traces_test.go.

167 test assertions green; build/vet/gofmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x CI

- Remove the committed porterw binary (24MB) and the 88 built UI assets under
  porterui/build/ from version control; keep a placeholder index.html so the
  go:embed still compiles, and gitignore the rest (rebuilt via `make ui`).
- Move scattered root docs into docs/ (journalctl-examples, mysql-auth, and the
  internal plan -> docs/ROADMAP.md).
- Trim the internal "Engineering Review" section from the README (its findings
  are addressed); keep the public-facing content.
- Add CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, .golangci.yml.
- CI: run on push/PR (not just manual), build/test the whole module (was only
  ./porterui/...), add golangci-lint + gofmt gates.
- Makefile: add check/test/lint/fmt/ui targets; stage the UI build for both the
  CLI and the porterui embed.
- gofmt the whole module clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The monolithic switch in executor.go (executor.dispatch) is replaced by an
action registry: each action is a small handler registered in init() and looked
up by name. Handlers are grouped into cohesive actions_*.go files (file, ensure,
assert, secret, exec, archive, pkg, net, systemd, docker, wait, io, build,
sysinfo).

Behavior-preserving: handler bodies are byte-identical to the former case
bodies, and the dispatch-level action set matches exactly (139 cases -> 139
registered handlers, verified by diff). register() panics on a duplicate name
so copy-paste mistakes surface at startup. executor.go drops from 1943 to 1205
lines.

Build, vet, and all 167 test assertions remain green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/ + cmd/porter-ui

Make the importable library (root package porter) the product, and move the
optional web dashboard out of the way:

- porterui/  -> web/        (package porterui -> package web)
- ui/        -> web/ui/     (React frontend under the dashboard subtree)
- cmd/porter -> cmd/porter-ui  (it is the dashboard server, not a library CLI)

Fixes a latent build defect: cmd's `//go:embed build/*` had no committed
placeholder, so a fresh clone couldn't `go build ./...`. Both embed sites
(web/ and cmd/porter-ui/) now ship a tracked placeholder index.html; the real
UI is built with `make ui`.

Updates Makefile, Dockerfile, .dockerignore, .gitignore, README, and the
package doc for the new paths and the porter-ui binary name. README now states
auth is on by default with a generated admin password (the admin/admin note was
stale). doc.go documents the full modern API surface.

Build, vet, and all tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tions

- Add docs/architecture.md: repo layout, core flow, the action registry and how
  to add an action, idempotency/change accounting, transport security,
  observability, and the optional dashboard.
- Replace the internal audit log at docs/ROADMAP.md with a public roadmap
  (done / considered next / non-goals); remove internal names and references.
- Fix stale porterui references and import paths in web/README.md.
- CHANGELOG: document the modern API surface, the registry refactor, the
  repository restructure, and the security changes.
- README: add Documentation, Contributing, and License sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ss-compiled)

Prevents a built binary at the root from being committed while not matching the
cmd/porter-ui source directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run staticcheck across the tree and fix all findings:
- rsync.go: convert RsyncBuilder to TaskBuilder instead of a struct literal (S1016)
- web/agent_ai.go: collapse if-return-bool (S1008), lowercase error strings
  (ST1005), variadic append instead of append-in-loop (S1011), and remove the
  dead callOpenAI/callAnthropic (kept-but-policy-blocked) helpers (U1000)
- web/machines.go: remove the unused legacy uploadFileToMachine (U1000)
- web/scheduler.go: remove the unused updateJobStatus, superseded by
  updateJobStatusWithError (U1000)
- web/dashboard_ws.go: drop the unused closed/mu fields on DashboardClient (U1000)

Build, vet, staticcheck, and all tests are clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran `gopls modernize -fix ./...` over the whole tree: interface{} -> any,
strings.SplitSeq range, and other current Go idioms. Mechanical, tool-verified;
build, vet, staticcheck, and all tests remain clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From a per-domain audit of the shell commands each action emits, apply the
no-behavior-surprise fixes:

- apt: set DEBIAN_FRONTEND=noninteractive on install/remove/upgrade (was only on
  the ensure path) so an unattended deploy can't hang on a debconf prompt.
- tar: drop the verbose -v from create/extract (cf/xf/czf/xzf) — pure log noise
  in automation; the archive bytes are unchanged.
- curl: add --proto-redir =https --tlsv1.2 so a download can't be silently
  downgraded https->http on redirect (the initial URL scheme is unaffected).
- streaming.go / web/ssh.go: use errors.As for *ssh.ExitError instead of a bare
  type assertion (handles wrapped errors; modern Go idiom).

Behavior-changing suggestions from the audit (docker run --restart/--init/log
rotation, npm ci, go release flags, rsync -z default, useradd -m, full-upgrade)
are deliberately NOT flipped as defaults; tracked for opt-in modifiers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2026 audit flagged a plain `docker run -d` as missing the defaults a
long-lived service wants. Rather than change the default (behavior change),
add opt-in builder modifiers:

- .Restart(policy)  -> --restart <policy>        (survive daemon/host restart)
- .Init()           -> --init                    (reap zombies, forward signals)
- .LogRotate(sz, n) -> --log-opt max-size/max-file (cap json-file logs)

buildDockerRun learns the new opts; defaults are unchanged. Tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: add an "audited commands" capability bullet.
- CHANGELOG: note the safe command defaults (DEBIAN_FRONTEND, quiet tar, curl
  TLS), the modernize pass, and the new docker run modifiers.
- ROADMAP: track the behavior-changing audit findings as future opt-in
  modifiers (apt --no-install-recommends/full-upgrade, npm ci, release go-build
  flags, rsync -z, tar --zstd, useradd -m) rather than default flips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Booyaka101 Booyaka101 merged commit 506b7b8 into main Jun 10, 2026
1 of 2 checks passed
@Booyaka101 Booyaka101 deleted the security-hardening-phase0 branch June 10, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants