chore(cleanup,docs): vite proxy + dead scheduler setter wired + registerAgent/CLI docs (C-1 master)

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.
This commit is contained in:
shankar0123
2026-04-25 17:34:59 +00:00
parent a39f5af22a
commit 1c6009a920
6 changed files with 51 additions and 2 deletions
+10
View File
@@ -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())
+2
View File
@@ -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).
+4
View File
@@ -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 |
+15
View File
@@ -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"),
+11
View File
@@ -248,6 +248,17 @@ export const getAgents = (params: Record<string, string> = {}) => {
export const getAgent = (id: string) =>
fetchJSON<Agent>(`${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<Agent>) =>
fetchJSON<Agent>(`${BASE}/agents`, { method: 'POST', body: JSON.stringify(data) });
+9 -2
View File
@@ -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: {