From ffc982bfbcc063d73e36829979d8a5bfa09fe60f Mon Sep 17 00:00:00 2001 From: certctl-bot Date: Sat, 25 Apr 2026 17:34:59 +0000 Subject: [PATCH] chore(cleanup,docs): vite proxy + dead scheduler setter wired + registerAgent/CLI docs (C-1 master) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes six 2026-04-24 audit findings (3 P2 + 3 P3) — a cleanup-and-doc tail bundle that drains the smallest remaining leaves of the audit: - cat-u-vite_dev_proxy_plaintext_drift (P2): web/vite.config.ts proxied dev requests to http://localhost:8443 against an HTTPS-only backend (HTTPS-only since v2.0.47). Every dev-server API call 502'd. Fix: targets are now object-form `{target: 'https://...', secure: false, changeOrigin: true}` — the dev cert is self-signed by the deploy/test bootstrap and changes per-checkout. - cat-g-7e38f9708e20 (P3): Scheduler.SetShortLivedExpiryCheckInterval was defined + tested but never called from cmd/server/main.go. Operators tuning CERTCTL_SHORT_LIVED_EXPIRY_CHECK_INTERVAL got no effect — the 30s default in scheduler.NewScheduler was effectively hardcoded. Fix: added Config.Scheduler.ShortLivedExpiryCheckInterval + getEnvDuration in Load() reading the env var with a 30s default, + sched.SetShortLivedExpiryCheckInterval(...) call in main.go alongside the other scheduler-interval setters. - diff-10xmain-2bf4a0a60388 (P3): same root cause as cat-g-7e38f9708e20; closes as ride-along. - cat-b-6177f36636fb (P2): registerAgent client fn orphan. By-design per pull-only deployment model. Fix (audit recommendation: "document"): added a closure docblock above the export in client.ts + a new "Registration is by-design pull-only" paragraph in docs/architecture.md::Agents section explaining when/why a future GUI-driven enrollment feature might reach the endpoint (proxy-agent topologies for network appliances). - cat-i-7c8b28936e3d (P2): CLI scope intentionally narrow but undocumented. Fix: new "Scope (intentionally narrow)" subsection in docs/features.md::CLI capturing the SSH-into-prod / day-to-day GUI / AI-automation MCP three-way split. Verification: - go build ./... — clean - go vet ./... — clean - go test ./internal/scheduler/... ./internal/config/... — pass - golangci-lint v2.11.4 run ./... — 0 issues - tsc --noEmit (frontend) — clean - All sibling guardrails (S-1 / G-3 / D-1+D-2 / B-1 / L-1 / H-1) still pass Audit findings closed: - cat-u-vite_dev_proxy_plaintext_drift (P2) - cat-g-7e38f9708e20 (P3) - diff-10xmain-2bf4a0a60388 (P3) - cat-b-6177f36636fb (P2) - cat-i-7c8b28936e3d (P2) - (audit-bookkeeping ride-along: ensures every closed-bundle row has a non-empty merge SHA) Deferred follow-ups: none from this bundle. The remaining audit backlog (frontend test campaign, F-1 CertificatesPage UX, P-1 orphan-fn sweep, S-2 handler error-mapping refactor) is sibling sub-bundles in this mega-prompt. --- cmd/server/main.go | 10 ++++++++++ docs/architecture.md | 2 ++ docs/features.md | 4 ++++ internal/config/config.go | 15 +++++++++++++++ web/src/api/client.ts | 11 +++++++++++ web/vite.config.ts | 11 +++++++++-- 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 475b01a..0133ce9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -545,6 +545,16 @@ func main() { // because they share the NotificationServicer dependency (same placement // pattern as I-001's SetJobRetryInterval above). sched.SetNotificationRetryInterval(cfg.Scheduler.NotificationRetryInterval) + // C-1 closure (cat-g-7e38f9708e20 + diff-10xmain-2bf4a0a60388): pre-C-1 + // the SetShortLivedExpiryCheckInterval setter was defined + tested but + // never called from main.go, so the 30-second hardcoded default in + // scheduler.NewScheduler was effectively the only value. Operators + // running short-lived cert workloads with high churn (or low-churn + // workloads wanting to relax the cadence) had no working knob despite + // CERTCTL_SHORT_LIVED_EXPIRY_CHECK_INTERVAL being documented. Wire it + // here alongside the other scheduler-interval setters so the + // documented env var actually takes effect. + sched.SetShortLivedExpiryCheckInterval(cfg.Scheduler.ShortLivedExpiryCheckInterval) if cfg.NetworkScan.Enabled { sched.SetNetworkScanInterval(cfg.NetworkScan.ScanInterval) logger.Info("network scanning enabled", "interval", cfg.NetworkScan.ScanInterval.String()) diff --git a/docs/architecture.md b/docs/architecture.md index c3caf40..f30de35 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -149,6 +149,8 @@ The agent runs two background loops: a heartbeat (every 60 seconds) to signal it Retired agents receive `410 Gone` on subsequent heartbeats (`service.ErrAgentRetired`). `cmd/agent` treats 410 as a terminal signal and exits cleanly so retired agents stop phoning home. Migration `000015` flipped `deployment_targets.agent_id` from `ON DELETE CASCADE` to `ON DELETE RESTRICT`, making the old hard-delete path a schema error and forcing all retirement through this contract. +**Registration is by-design pull-only (C-1 closure, cat-b-6177f36636fb).** Agents register themselves at first heartbeat via `install-agent.sh` + `cmd/agent/main.go` — never via the GUI. The `web/src/api/client.ts::registerAgent` client function is intentionally orphan in the dashboard for this reason. It's preserved in `client.ts` (rather than deleted) so future features that want to drive registration from the GUI — for example, a one-click "register proxy agent" panel for network-appliance topologies where the agent runs in a different network zone from the device it manages — can reach the endpoint without a `client.ts` edit. Operators looking to scale agent enrollment use `install-agent.sh` against a config-management system (Ansible, Salt, Puppet) or a baked-in cloud-init script, not the dashboard. + ### Web Dashboard The web dashboard is the primary operational interface for certctl. It is built with Vite + React + TypeScript and uses TanStack Query for server state management (caching, background refetching, optimistic updates). diff --git a/docs/features.md b/docs/features.md index 6c2f0f3..aa09858 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1220,6 +1220,10 @@ Latching state prevents refetch-driven dismissal. `localStorage` dismissal key: `certctl-cli` — stdlib-only (`flag` + `text/tabwriter`), no Cobra dependency. +### Scope (intentionally narrow) + +The CLI focuses on **read-heavy operator triage** (list, get, status, version) and **bulk-action surface** (`certs bulk-revoke`, `import`). It deliberately omits admin CRUD for issuers, targets, owners, teams, agent groups, certificate profiles, renewal policies, policy rules, and notifications — those live in the GUI and the MCP server (rebuild count via `grep -cE 'gomcp\.AddTool\(' internal/mcp/tools.go` for the full operator surface). This split is intentional: CLI is the SSH-into-the-prod-host emergency console; GUI is the day-to-day operator console; MCP is the AI/automation surface. Closes audit finding `cat-i-7c8b28936e3d` — pre-this-doc the narrow scope was correct in code but confused readers who scanned `docs/features.md`'s "CLI commands" count and assumed the CLI was incomplete. + ### Commands | Command | Description | diff --git a/internal/config/config.go b/internal/config/config.go index 866d3fb..3339287 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -784,6 +784,18 @@ type SchedulerConfig struct { // second. // Setting: CERTCTL_JOB_AWAITING_APPROVAL_TIMEOUT environment variable. AwaitingApprovalTimeout time.Duration + + // ShortLivedExpiryCheckInterval is how often the scheduler scans + // short-lived certificates and marks expired rows as Expired. Default: + // 30 seconds (matches the in-memory default in scheduler.NewScheduler). + // C-1 closure (cat-g-7e38f9708e20 + diff-10xmain-2bf4a0a60388): + // pre-C-1 the setter scheduler.SetShortLivedExpiryCheckInterval was + // defined + tested but never called from cmd/server/main.go, so the + // 30-second default was effectively hardcoded. Operators who needed + // to tune the cadence (e.g. a high-churn short-lived cert tenant) + // had no path. Post-C-1 main.go wires this knob. + // Setting: CERTCTL_SHORT_LIVED_EXPIRY_CHECK_INTERVAL environment variable. + ShortLivedExpiryCheckInterval time.Duration } // LogConfig contains logging configuration. @@ -948,6 +960,9 @@ func Load() (*Config, error) { JobTimeoutInterval: getEnvDuration("CERTCTL_JOB_TIMEOUT_INTERVAL", 10*time.Minute), AwaitingCSRTimeout: getEnvDuration("CERTCTL_JOB_AWAITING_CSR_TIMEOUT", 24*time.Hour), AwaitingApprovalTimeout: getEnvDuration("CERTCTL_JOB_AWAITING_APPROVAL_TIMEOUT", 168*time.Hour), + // C-1 closure: matches the in-memory default at + // internal/scheduler/scheduler.go:145 (30 * time.Second). + ShortLivedExpiryCheckInterval: getEnvDuration("CERTCTL_SHORT_LIVED_EXPIRY_CHECK_INTERVAL", 30*time.Second), }, Log: LogConfig{ Level: getEnv("CERTCTL_LOG_LEVEL", "info"), diff --git a/web/src/api/client.ts b/web/src/api/client.ts index fefe97a..0604184 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -248,6 +248,17 @@ export const getAgents = (params: Record = {}) => { export const getAgent = (id: string) => fetchJSON(`${BASE}/agents/${id}`); +// C-1 closure (cat-b-6177f36636fb): registerAgent is intentionally +// orphan in the GUI per certctl's pull-only deployment model. Agents +// enroll via install-agent.sh + cmd/agent/main.go and register +// themselves at first heartbeat — operators don't (and shouldn't) +// drive registration from the dashboard. The client fn is preserved +// here (rather than deleted) so future features that want to drive +// registration from the GUI (e.g. a one-click "register proxy agent" +// panel for network-appliance topologies) can reach the endpoint +// without a client.ts edit. See docs/architecture.md::Agents for +// the architectural rationale and unified-audit.md cat-b-6177f36636fb +// for closure rationale. export const registerAgent = (data: Partial) => fetchJSON(`${BASE}/agents`, { method: 'POST', body: JSON.stringify(data) }); diff --git a/web/vite.config.ts b/web/vite.config.ts index f477cce..3a06a84 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,13 +1,20 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// C-1 closure (cat-u-vite_dev_proxy_plaintext_drift): pre-C-1 the dev +// proxy targeted http://localhost:8443 against an HTTPS-only backend +// (HTTPS-only since v2.0.47 — see docs/tls.md). Every dev-server API +// call 502'd. Post-C-1 the proxy targets https:// with secure:false +// because the dev cert is self-signed by deploy/test bootstrap and +// changes per-checkout — production stops validation at the reverse +// proxy or load balancer, not the Vite dev server. export default defineConfig({ plugins: [react()], server: { port: 5173, proxy: { - '/api': 'http://localhost:8443', - '/health': 'http://localhost:8443', + '/api': { target: 'https://localhost:8443', secure: false, changeOrigin: true }, + '/health': { target: 'https://localhost:8443', secure: false, changeOrigin: true }, } }, build: {