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: {