Task · SKT-0005.12

Harden control-plane exposure and local file permissions

Description

Protect operational surfaces when users move beyond loopback and when synthkit runs on shared hosts. Mutation Basic auth is plaintext without TLS, read routes expose topology, and setup/staging permissions are broader than necessary. Correct inaccurate claims about persisted git-token values.

Acceptance Criteria

Definition of Done

Implementation Plan

Run contract: front-loaded security wave; root owns design, integration, tracker, commits, pushes, and all writes. Topology is one read-only SECURITY review auxiliary plus root implementation because bind/auth/filesystem behavior crosses several shared seams and benefits from adversarial challenge; no auxiliary edits files.

  1. Write failing tests for effective exposure detection: direct runs use JSON_HTTP_ADDR, containers use SYNTHKIT_BIND; loopback remains compatible, while non-loopback requires CONTROL_TOKEN plus CONTROL_EXPOSURE_ACK set exactly to trusted-network or tls-proxy. Invalid acknowledgements fail clearly.
  2. Implement the startup gate and document the intentional compatibility break for existing non-loopback deployments. Keep /healthz, /control/readiness, and /control/status unauthenticated for probes; when CONTROL_TOKEN is configured, require Basic auth for the operator UI, derived/control topology GETs, raw blueprint/config/state endpoints, blueprint-source lifecycle reads, and Infinity JSON data routes. Update the customer Infinity setup to use datasource Basic auth and prohibit Basic credentials over plaintext untrusted networks.
  3. Harden writable operational storage to owner-only modes: setup creates .env at 0600 and the state volume at 0700; runtime-created state, manifest, custom, and fetched blueprint files use 0600 and their directories 0700, including tightening existing writable paths when safe. Correct every claim that resolved git token values persist; only token_env_var names persist.
  4. Run focused config/startup/control/jsondata/bpsource and setup-helper tests, regenerate env-alignment surfaces as required, then run a security review across bind, auth, read exposure, permissions, and compatibility. Fix Critical/Warning findings, run CodeRabbit, make gate, and dry-run inventory before finalization.

Security-review corrections: 5. Pass the exact Compose-interpolated SYNTHKIT_BIND into the container and fail closed when a container cannot determine its effective host bind. Run the exposure gate immediately after config load/live validation and before profiling, state probes, git/network activity, or self-observability; -once/-dump and -preflight do not serve HTTP and remain usable without an acknowledgement. 6. Protect /control/status as topology-bearing. Keep only /healthz and a sanitized /control/readiness response public; OPTIONS remains unauthenticated for CORS. Protect every other control/UI GET and every Infinity JSON route when CONTROL_TOKEN is configured. Datasource Basic auth covers server-side Infinity reads only; browser-direct action buttons retain their separate Basic challenge flow and require an HTTPS browser E2E before finalization. 7. Permission hardening rejects symlink/non-directory surprises, tightens pre-existing dedicated data paths, and uses owner-only temporary files plus atomic rename so a planted destination symlink is never followed. Do not chmod the repository root for the direct snapshot default. Verify the full exposure, auth-route, permissions, persistence, and loopback-backward-compatibility matrices from the security review.

Supersession note: original step 2’s statement that /control/status stays unauthenticated is replaced by security-review corrections 6-7. Only /healthz and sanitized /control/readiness remain public; status and every other sensitive control/Infinity read require Basic auth when CONTROL_TOKEN is configured.

Implementation Notes

Research baseline: the binary currently only warns on a non-loopback unauthenticated bind; Compose always binds 0.0.0.0 inside the container and effective exposure is controlled by SYNTHKIT_BIND. All control GETs and every Infinity JSON route are open today, including raw YAML, derived topology, redacted endpoint/path configuration, request correlation IDs, and git source metadata. Setup helpers publish .env as 0600 after their first write, but the skill initially copies .env.example at its 0644 mode. Custom/git staging directories and files are currently 0755/0644; control snapshots already use a 0600 CreateTemp but existing paths are not proactively tightened. docs/security.md incorrectly claims resolved git token values are persisted. Adversarial security plan review found two critical corrections: Compose shell interpolation can publish a different SYNTHKIT_BIND than env_file exposes inside the container, and the current warning occurs after profiler/state/git/self-observability side effects. It also required protecting /control/status, sanitizing public readiness, distinguishing datasource auth from browser action auth, hardening existing paths and symlinks rather than creation modes alone, and testing exact acknowledgement parsing plus loopback compatibility. These findings are incorporated into the appended plan; implementation awaits approval of the material exposure/auth policy. Implemented the approved fail-closed exposure contract, protected sensitive control/Infinity reads, sanitized public readiness, owner-only atomic blueprint/state writes, exact Compose bind propagation, secure Infinity provisioning, and corrected token_env_var persistence documentation. Adversarial follow-up found and fixed three additional regressions with observed red-green tests: HEAD read-auth bypass, authenticated Infinity CORS preflight, and legacy staged YAML modes on cached fetch/startup resolution. Focused Go packages, setup-helper suite, dashboard generator test, shell syntax, and git diff check are green; final CodeRabbit, make gate, dry-run inventory, exact-head CI, and standing-host deployment remain. Final local evidence: CodeRabbit initial review raised 2 Major/4 Minor; both Majors and all valid Minors were fixed. Confirmation review raised 0 Critical/Warning and 2 advisory Minors; ordered setup-block validation was applied, while duplicate container integration coverage was dismissed because unit/config tests already cover both acknowledgements, missing/effective binds, loopback compatibility, and exact Compose propagation. make gate passed in full. DRY_RUN=true go run ./cmd/synthkit -once -dump exited 0 with 26 blueprints and complete metrics/logs/traces/Sigil/Pyroscope inventory; absent optional Faro credentials were correctly reported disabled.

Exact-head CI run 32401706899 passed hygiene, Docker, UI, secret scan, Go (including race), E2E, and aggregate success for 35942c0. Auto-RC run 32401707676 published 1.3.0-rc.16; its tag resolves to 35942c0 and the deployed OCI image carries revision 35942c0 with digest sha256:0b08fc682f164003274051992f0c5b75483791676dd351c44c7ab62383e866be. Standing-host Docker dry-run passed for the selected otlp-native blueprint with 664 metric names plus logs, traces, and native OTLP metrics, then live deployment became healthy with DRY_RUN=false. Public readiness is sanitized; unauthenticated sensitive GET and HEAD routes return 401; authenticated status/UI/Infinity reads succeed; control and Infinity OPTIONS return 204 with the expected CORS allow headers. The state tree is 0700/0600 under uid 65532. All promrw, loki, otlp, and otlpmetrics lanes report fresh successful pushes with zero failures, and explicit intended-context Grafana queries returned fresh native OTLP metrics, logs, and traces. Tailscale Serve provides trusted HTTPS and curl verifies its certificate, but the required action-button browser E2E remains unverified: both connected Chrome profiles return ERR_BLOCKED_BY_CLIENT for every tailnet URL, including public readiness, and the in-app browser surface is unavailable. Do not finalize until an operator opens the authenticated HTTPS UI manually (or the browser block is removed) and the current 1x volume preset is clicked as a no-op POST with no action error.

Live self-observability follow-up (2026-08-20): the operator explicitly authorized this deployment to reuse the existing synthetic OTLP endpoint, tenant user, and ingestion credential for the separate GC_SELF_OTLP_* runtime surface; no additional token was minted. The deployment was returned from dry-run to live mode and recreated from 1.3.0-rc.16 at revision 35942c0. Docker reported healthy, the safe runtime check confirmed DRY_RUN=false and SELFOBS_ENABLED=true with all three self-observability variables present, and startup reported metrics+traces+logs at the 15s metric interval. Explicit intended-context queries after restart returned fresh non-empty synthkit_* metrics for service version 1.3.0-rc.16, fresh self-observability log records with telemetry_kind=self-observability, and fresh traces rooted at service synthkit including tick, flush loki, and flush otlp. Container logs contained no self-observability export, authorization, or authentication errors. Process profiling remains intentionally disabled because its separate optional Pyroscope triplet is unset. The task remains In Progress: the authenticated HTTPS action-button browser E2E is still blocked by the available browser clients and must be completed before finalization.

Final browser acceptance (2026-08-20): the operator manually logged into the authenticated control UI over HTTPS, exercised the outstanding action-button flow, and confirmed it worked successfully. This closes the final browser-specific verification gap.

Final Summary

Hardened non-loopback control-plane exposure, authenticated all sensitive control and Infinity reads, sanitized public readiness, and enforced owner-only operational storage with safe atomic writes. Corrected credential and persisted-token documentation, deployed the exact published image to the standing host, and verified health, authentication/CORS behavior, file modes, every configured telemetry sink, and self-observability metrics/logs/traces. Automated evidence includes the full local gate, dry-run inventory, and exact-head CI; the operator completed the final authenticated HTTPS action-button browser test successfully.

References

View the source file on GitHub