diff --git a/internal/config/config.go b/internal/config/config.go index defad6d..6f13f96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "log/slog" - "net" "os" "strconv" "strings" @@ -621,193 +620,6 @@ type VerificationConfig struct { Delay time.Duration // Wait before verification after deployment (default 2s) } -// ServerConfig contains HTTP server configuration. -type ServerConfig struct { - Host string // Server host (default: 127.0.0.1). Set via CERTCTL_SERVER_HOST. - Port int // Server port (default: 8080). Set via CERTCTL_SERVER_PORT. - MaxBodySize int64 // Maximum request body size in bytes (default: 1MB). Set via CERTCTL_MAX_BODY_SIZE. - TLS ServerTLSConfig // HTTPS-only TLS configuration. Both CertPath and KeyPath are required. - - // AuditFlushTimeoutSeconds is the budget (in seconds) main.go gives the - // audit middleware to drain in-flight recordings during graceful - // shutdown. Bundle-5 / Audit M-011: pre-Bundle-5 this was hard-coded - // 30s, which dropped events silently in high-volume environments - // because the same context governed HTTP server shutdown + audit - // flush. Post-Bundle-5: configurable; default 30s preserves prior - // behaviour. WARN-log on deadline exceeded, but never exit hard. - // Setting: CERTCTL_AUDIT_FLUSH_TIMEOUT_SECONDS environment variable. - AuditFlushTimeoutSeconds int -} - -// ServerTLSConfig holds the server-side TLS material. -// -// The control plane is HTTPS-only as of the HTTPS-everywhere milestone -// (§3 locked decisions: no `http` mode, no dual-listener, TLS 1.3 only). -// Both CertPath and KeyPath are required; an empty value causes -// Config.Validate() to return a fail-loud error and the server refuses -// to start. There is no plaintext HTTP fallback, no N-release migration -// bridge, and no auto-generated self-signed cert — operators either -// supply a cert on disk (docker-compose init container, operator-managed -// file, cert-manager mount) or the process exits non-zero. -type ServerTLSConfig struct { - // CertPath is the filesystem path to the server's PEM-encoded X.509 - // certificate. Set via CERTCTL_SERVER_TLS_CERT_PATH. Required. - CertPath string - - // KeyPath is the filesystem path to the server's PEM-encoded private - // key that signs CertPath. Set via CERTCTL_SERVER_TLS_KEY_PATH. Required. - KeyPath string -} - -// DatabaseConfig contains database connection configuration. -type DatabaseConfig struct { - URL string - MaxConnections int - MigrationsPath string - - // DemoSeed, when true, makes the server apply - // `/seed_demo.sql` after the baseline `seed.sql`. Set - // via CERTCTL_DEMO_SEED. The compose demo overlay - // (deploy/docker-compose.demo.yml) sets this to keep the demo path - // alive after U-3 dropped initdb-mounted seed files. The seed file - // uses ON CONFLICT (id) DO NOTHING so re-running on a populated - // database is safe; missing-file is a no-op (returns nil) so a - // minimal-image deploy that strips seed_demo.sql still boots cleanly. - DemoSeed bool -} - -// SchedulerConfig contains scheduler timing configuration. -type SchedulerConfig struct { - // RenewalCheckInterval is how often the renewal scheduler checks for expiring certs. - // Default: 1 hour. Minimum: 1 minute. Certs are flagged for renewal at configured thresholds. - // Setting: CERTCTL_SCHEDULER_RENEWAL_CHECK_INTERVAL environment variable. - RenewalCheckInterval time.Duration - - // JobProcessorInterval is how often the job scheduler processes pending jobs. - // Default: 30 seconds. Minimum: 1 second. Controls issuance, renewal, and deployment latency. - // Setting: CERTCTL_SCHEDULER_JOB_PROCESSOR_INTERVAL environment variable. - JobProcessorInterval time.Duration - - // RenewalConcurrency caps the number of concurrent renewal/issuance/ - // deployment goroutines launched per job-processor tick. Default 25 — - // high enough to make use of HTTP/1.1 connection reuse against an - // upstream CA, low enough to stay under typical per-customer rate - // limits. Operators with permissive upstream limits and large fleets - // (>10k certs) can bump to 100; operators with strict limits or - // async-CA-heavy fleets should keep at 25 or lower. - // - // Values ≤ 0 fall back to 1 (sequential) — fail-safe rather than - // panicking on semaphore.NewWeighted(0) semantics. - // - // Closes the #9 acquisition-readiness blocker from the 2026-05-01 - // issuer coverage audit. Pre-fix the per-tick fan-out had no cap, - // so a 5k-cert sweep launched 5k in-flight HTTP calls to upstream - // CAs and tripped DigiCert/Entrust/Sectigo rate limits. - // - // Setting: CERTCTL_RENEWAL_CONCURRENCY environment variable. - RenewalConcurrency int - - // AgentHealthCheckInterval is how often the scheduler checks agent heartbeats. - // Default: 2 minutes. Minimum: 1 second. Marks agents offline if no recent heartbeat. - // Setting: CERTCTL_SCHEDULER_AGENT_HEALTH_CHECK_INTERVAL environment variable. - AgentHealthCheckInterval time.Duration - - // NotificationProcessInterval is how often the scheduler processes pending notifications. - // Default: 1 minute. Minimum: 1 second. Sends notifications to Slack, Teams, PagerDuty, etc. - // Setting: CERTCTL_SCHEDULER_NOTIFICATION_PROCESS_INTERVAL environment variable. - NotificationProcessInterval time.Duration - - // NotificationRetryInterval is how often the scheduler retries failed - // notifications whose retry_count is below the service-layer 5-attempt - // DLQ budget. Default: 2 minutes. Minimum: 1 second. Mirrors the I-001 - // RetryInterval knob: transitions eligible Failed notifications whose - // next_retry_at has arrived back to Pending so the notification processor - // picks them up on its next tick (closes coverage gap I-005 — HEAD had - // no retry path for transient SMTP/webhook failures and notifications - // stayed Failed forever). - // Setting: CERTCTL_NOTIFICATION_RETRY_INTERVAL environment variable. - NotificationRetryInterval time.Duration - - // RetryInterval is how often the scheduler retries failed jobs whose Attempts - // counter is below MaxAttempts. Default: 5 minutes. Minimum: 1 second. - // Transitions eligible Failed jobs back to Pending so the job processor can - // pick them up again (closes coverage gap I-001 — JobService.RetryFailedJobs - // had no caller prior to this loop being wired). - // Setting: CERTCTL_SCHEDULER_RETRY_INTERVAL environment variable. - RetryInterval time.Duration - - // JobTimeoutInterval is how often the reaper loop sweeps AwaitingCSR and - // AwaitingApproval jobs for TTL expiration. Default: 10 minutes. Minimum: 1 - // second. Timed-out jobs are transitioned to Failed with a descriptive error - // message; I-001's retry loop then auto-promotes eligible Failed jobs back - // to Pending (closes coverage gap I-003). - // Setting: CERTCTL_JOB_TIMEOUT_INTERVAL environment variable. - JobTimeoutInterval time.Duration - - // AwaitingCSRTimeout is the maximum age an AwaitingCSR job can remain in - // that state before the reaper transitions it to Failed. Default: 24 hours. - // An agent that hasn't submitted a CSR within this window is presumed - // unreachable. Minimum: 1 second. - // Setting: CERTCTL_JOB_AWAITING_CSR_TIMEOUT environment variable. - AwaitingCSRTimeout time.Duration - - // AwaitingApprovalTimeout is the maximum age an AwaitingApproval job can - // remain in that state before the reaper transitions it to Failed. Default: - // 168 hours (7 days). Reviewers who haven't approved within this window - // force the renewal to fail loudly rather than silently stall. Minimum: 1 - // 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 - - // CRLGenerationInterval is how often the scheduler pre-generates - // CRLs into the crl_cache table. The /.well-known/pki/crl/{issuer_id} - // HTTP endpoint reads from this cache instead of regenerating per - // request. Default: 1 hour. - // Setting: CERTCTL_CRL_GENERATION_INTERVAL environment variable. - // Bundle CRL/OCSP-Responder Phase 3. - CRLGenerationInterval time.Duration - - // OCSPRateLimitPerIPMin is the per-source-IP cap on OCSP requests - // per minute. Defaults to 1000 (production hardening II Phase 3 - // frozen decision 0.5). Zero disables the limit. - // Setting: CERTCTL_OCSP_RATE_LIMIT_PER_IP_MIN environment variable. - OCSPRateLimitPerIPMin int - - // CertExportRateLimitPerActorHr is the per-actor cap on cert-export - // requests per hour. Defaults to 50 (production hardening II Phase - // 3 frozen decision 0.6). Zero disables the limit. - // Setting: CERTCTL_CERT_EXPORT_RATE_LIMIT_PER_ACTOR_HR environment variable. - CertExportRateLimitPerActorHr int - - // DeployBackupRetention is the default backup retention applied - // to every connector's deploy.Plan when the per-target config - // doesn't override. Defaults to 3 (deploy-hardening I frozen - // decision 0.2). Set to -1 to disable backups entirely (rollback - // becomes impossible — documented foot-gun). - // Setting: CERTCTL_DEPLOY_BACKUP_RETENTION environment variable. - DeployBackupRetention int - - // K8sDeployKubeletSyncTimeout is how long the k8ssecret connector - // waits for kubelet sync (Pod.Status.ContainerStatuses indicating - // the new Secret has been mounted) after a Secret update before - // timing out the post-deploy verify. Defaults to 60s. - // Setting: CERTCTL_K8S_DEPLOY_KUBELET_SYNC_TIMEOUT environment variable. - // Deploy-hardening I Phase 9. - K8sDeployKubeletSyncTimeout time.Duration -} - // ApprovalConfig contains issuance approval-workflow runtime configuration. // Rank 7 of the 2026-05-03 Infisical deep-research deliverable. type ApprovalConfig struct { @@ -831,67 +643,6 @@ type ApprovalConfig struct { BypassEnabled bool } -// LogConfig contains logging configuration. -type LogConfig struct { - // Level sets the minimum log level for output. - // Valid values: "debug" (verbose), "info" (default), "warn" (warnings), "error" (errors only). - // Setting: CERTCTL_LOG_LEVEL environment variable. Default: "info". - Level string - - // Format sets the output format for logs. - // Valid values: "json" (structured, for parsing), "text" (human-readable). - // Setting: CERTCTL_LOG_FORMAT environment variable. Default: "json". - Format string -} - -// RateLimitConfig contains rate limiting configuration. -// -// Bundle B / Audit M-025 (OWASP ASVS L2 §11.2.1): pre-bundle the rate -// limiter was global (a single token bucket shared across every request); -// post-bundle it is per-key with separate budgets for IP-keyed and -// user-keyed buckets. RPS / BurstSize are PER-KEY budgets. -type RateLimitConfig struct { - // Enabled controls whether rate limiting is enforced on API endpoints. - // Default: true. Set to false to disable rate limits (not recommended for production). - // Setting: CERTCTL_RATE_LIMIT_ENABLED environment variable. - Enabled bool - - // RPS is the target requests per second allowed PER KEY (token bucket - // rate). For unauthenticated callers the key is the source IP; for - // authenticated callers the key is the API-key name (UserKey context - // value populated by NewAuthWithNamedKeys). - // Default: 50. Higher values allow burst throughput; lower values restrict load. - // Setting: CERTCTL_RATE_LIMIT_RPS environment variable. - RPS float64 - - // BurstSize is the maximum number of requests allowed in a single burst. - // Default: 100. Allows clients to exceed RPS briefly when BurstSize tokens available. - // Must be at least as large as RPS. Higher = more lenient burst handling. - // Setting: CERTCTL_RATE_LIMIT_BURST environment variable. - BurstSize int - - // PerUserRPS overrides RPS for authenticated callers. When zero, RPS is - // used for both keying dimensions. Set this higher than RPS to grant - // authenticated clients a more generous budget than anonymous probes. - // Default: 0 (use RPS). - // Setting: CERTCTL_RATE_LIMIT_PER_USER_RPS environment variable. - PerUserRPS float64 - - // PerUserBurstSize overrides BurstSize for authenticated callers. When - // zero, BurstSize is used. Default: 0 (use BurstSize). - // Setting: CERTCTL_RATE_LIMIT_PER_USER_BURST environment variable. - PerUserBurstSize int -} - -// CORSConfig contains CORS configuration. -type CORSConfig struct { - // AllowedOrigins is a list of allowed origins for CORS requests. - // Security default: empty list denies all CORS requests (same-origin only). - // ["*"] allows all origins (development/demo mode only, security risk). - // Specific origins (e.g., ["https://app.example.com"]) whitelist only those origins. - AllowedOrigins []string -} - // Load reads configuration from environment variables and returns a Config. // Environment variables must have the CERTCTL_ prefix. // Example: CERTCTL_SERVER_HOST, CERTCTL_DATABASE_URL, etc. @@ -1920,44 +1671,3 @@ func (c *Config) GetLogLevel() slog.Level { return slog.LevelInfo } } - -// isLoopbackAddr returns true when host is bound to a loopback -// interface only (127.0.0.1, ::1, or "localhost"). Used by the -// HIGH-12 demo-mode startup guard to refuse non-loopback binds when -// CERTCTL_AUTH_TYPE=none is in effect. -// -// "" (unset) AND "0.0.0.0" / "::" / "[::]" return false because those -// surface the listener to every interface — exactly the misconfiguration -// the guard is designed to catch. -// -// Hostnames other than "localhost" return false defensively: a hostname -// could resolve to a non-loopback IP at runtime; we don't perform DNS -// here because the guard runs at startup before any network state is -// available, and we don't want a misconfigured /etc/hosts to silently -// pass the guard. Operators wanting to bind to a non-default loopback -// alias must either use 127.0.0.1 / ::1 directly or set -// CERTCTL_DEMO_MODE_ACK=true. -func isLoopbackAddr(host string) bool { - switch host { - case "": - // Empty / unset host — Go's net/http.Server treats this as - // "all interfaces" (equivalent to 0.0.0.0). Surface it to the - // network → not loopback. - return false - case "0.0.0.0", "::", "[::]": - return false - case "localhost": - return true - } - // Strip a trailing :port if the operator passed a host:port pair - // rather than a bare host (defensive — Server.Host is documented - // as host-only, but be lenient). - if h, _, err := net.SplitHostPort(host); err == nil { - host = h - } - if ip := net.ParseIP(host); ip != nil { - return ip.IsLoopback() - } - // Hostname that isn't "localhost" — fail closed. - return false -} diff --git a/internal/config/server.go b/internal/config/server.go new file mode 100644 index 0000000..2aede91 --- /dev/null +++ b/internal/config/server.go @@ -0,0 +1,374 @@ +// Copyright 2026 certctl LLC. All rights reserved. +// SPDX-License-Identifier: BUSL-1.1 + +package config + +import ( + "net" + "time" +) + +// Phase 9 ARCH-M2 closure Sprint 6 (2026-05-14): extracted from +// config.go. Sprint 6 groups the server-tier infrastructure structs +// — the things that configure HOW the server runs (HTTP listener, +// TLS, DB pool, scheduler loops, log level, rate limiting, CORS) +// rather than WHAT it serves (issuer configs, ACME, SCEP, EST, +// auth identity). +// +// Seven structs + one unexported helper move: +// +// ServerConfig — HTTP listener (Host, Port, MaxBodySize, +// TLS sub-struct, AuditFlushTimeoutSeconds). +// ServerTLSConfig — HTTPS-only TLS material (CertPath + +// KeyPath). HTTPS-everywhere milestone: no +// plaintext fallback, no dual-listener. +// DatabaseConfig — DB connection + pool settings + DemoSeed +// toggle for the compose demo overlay. +// SchedulerConfig — all 15 scheduler-loop tunables + the +// per-tick concurrency cap + the deploy/ +// connector timeouts that ride the same +// env-var family. +// LogConfig — Level + Format (info/json defaults). +// RateLimitConfig — Bundle B / M-025: per-key token bucket +// with separate IP-keyed + user-keyed +// budgets. +// CORSConfig — AllowedOrigins (deny-by-default empty). +// +// isLoopbackAddr() — HIGH-12 demo-mode startup guard helper. +// Returns true ONLY for 127.0.0.1 / ::1 / +// "localhost"; everything else (including +// 0.0.0.0, ::, and hostnames that aren't +// "localhost") returns false. Same-package +// caller is Validate() in config.go which +// gates Type=none on non-loopback binds. +// Test caller in config_test.go is also +// package `config` so the unexported callable +// surface stays accessible. +// +// What stayed in config.go +// ======================== +// - ApprovalConfig — RBAC-related (issuance-approval workflow), not +// server-tier infrastructure. Sits between SchedulerConfig and +// LogConfig in the original file ordering; Sprint 6's two-pass +// sed deliberately preserves it where it is. Candidate for a +// future Auth/RBAC follow-up cut if the operator wants the +// approval surface adjacent to AuthConfig. +// - The Validate() body that uses isLoopbackAddr to gate +// CERTCTL_AUTH_TYPE=none — cross-cutting validation logic stays +// in config.go. +// - The Load() body that synthesizes ServerConfig / ServerTLSConfig +// / DatabaseConfig / SchedulerConfig / LogConfig / RateLimitConfig +// / CORSConfig from env vars via the shared getEnv* helpers. +// - The shared getEnv* helpers (getEnv / getEnvBool / getEnvInt / +// getEnvDuration / getEnvFloat / getEnvInt64 / getEnvList). +// +// Import-graph hygiene +// ==================== +// isLoopbackAddr is the ONLY user of the `net` package in config.go. +// After this move, config.go's `net` import becomes unused; the +// Sprint 6 commit removes it from config.go's import block. server.go +// imports `net` directly. The `time` import in config.go stays +// because other configs (notably ApprovalConfig isn't time-typed but +// SCEP/EST helpers in their respective .go files import their own +// `time`; config.go retains `time.Duration` uses in OCSPResponderConfig, +// DigestConfig, HealthCheckConfig, NetworkScanConfig, VerificationConfig, +// and the various issuer-specific configs that haven't been split yet). +// +// Public-surface invariant +// ======================== +// Every type, exported field, and doc-comment is byte-identical to +// pre-split. Package stays `config`. Every external caller of +// `config.ServerConfig` / `config.ServerTLSConfig` / etc. resolves +// the same way. The unexported `isLoopbackAddr` is invisible to +// package consumers; its same-package caller (Validate in config.go) +// + its test (config_test.go in package `config`) continue to +// resolve via the package symbol table. + +// ServerConfig contains HTTP server configuration. +type ServerConfig struct { + Host string // Server host (default: 127.0.0.1). Set via CERTCTL_SERVER_HOST. + Port int // Server port (default: 8080). Set via CERTCTL_SERVER_PORT. + MaxBodySize int64 // Maximum request body size in bytes (default: 1MB). Set via CERTCTL_MAX_BODY_SIZE. + TLS ServerTLSConfig // HTTPS-only TLS configuration. Both CertPath and KeyPath are required. + + // AuditFlushTimeoutSeconds is the budget (in seconds) main.go gives the + // audit middleware to drain in-flight recordings during graceful + // shutdown. Bundle-5 / Audit M-011: pre-Bundle-5 this was hard-coded + // 30s, which dropped events silently in high-volume environments + // because the same context governed HTTP server shutdown + audit + // flush. Post-Bundle-5: configurable; default 30s preserves prior + // behaviour. WARN-log on deadline exceeded, but never exit hard. + // Setting: CERTCTL_AUDIT_FLUSH_TIMEOUT_SECONDS environment variable. + AuditFlushTimeoutSeconds int +} + +// ServerTLSConfig holds the server-side TLS material. +// +// The control plane is HTTPS-only as of the HTTPS-everywhere milestone +// (§3 locked decisions: no `http` mode, no dual-listener, TLS 1.3 only). +// Both CertPath and KeyPath are required; an empty value causes +// Config.Validate() to return a fail-loud error and the server refuses +// to start. There is no plaintext HTTP fallback, no N-release migration +// bridge, and no auto-generated self-signed cert — operators either +// supply a cert on disk (docker-compose init container, operator-managed +// file, cert-manager mount) or the process exits non-zero. +type ServerTLSConfig struct { + // CertPath is the filesystem path to the server's PEM-encoded X.509 + // certificate. Set via CERTCTL_SERVER_TLS_CERT_PATH. Required. + CertPath string + + // KeyPath is the filesystem path to the server's PEM-encoded private + // key that signs CertPath. Set via CERTCTL_SERVER_TLS_KEY_PATH. Required. + KeyPath string +} + +// DatabaseConfig contains database connection configuration. +type DatabaseConfig struct { + URL string + MaxConnections int + MigrationsPath string + + // DemoSeed, when true, makes the server apply + // `/seed_demo.sql` after the baseline `seed.sql`. Set + // via CERTCTL_DEMO_SEED. The compose demo overlay + // (deploy/docker-compose.demo.yml) sets this to keep the demo path + // alive after U-3 dropped initdb-mounted seed files. The seed file + // uses ON CONFLICT (id) DO NOTHING so re-running on a populated + // database is safe; missing-file is a no-op (returns nil) so a + // minimal-image deploy that strips seed_demo.sql still boots cleanly. + DemoSeed bool +} + +// SchedulerConfig contains scheduler timing configuration. +type SchedulerConfig struct { + // RenewalCheckInterval is how often the renewal scheduler checks for expiring certs. + // Default: 1 hour. Minimum: 1 minute. Certs are flagged for renewal at configured thresholds. + // Setting: CERTCTL_SCHEDULER_RENEWAL_CHECK_INTERVAL environment variable. + RenewalCheckInterval time.Duration + + // JobProcessorInterval is how often the job scheduler processes pending jobs. + // Default: 30 seconds. Minimum: 1 second. Controls issuance, renewal, and deployment latency. + // Setting: CERTCTL_SCHEDULER_JOB_PROCESSOR_INTERVAL environment variable. + JobProcessorInterval time.Duration + + // RenewalConcurrency caps the number of concurrent renewal/issuance/ + // deployment goroutines launched per job-processor tick. Default 25 — + // high enough to make use of HTTP/1.1 connection reuse against an + // upstream CA, low enough to stay under typical per-customer rate + // limits. Operators with permissive upstream limits and large fleets + // (>10k certs) can bump to 100; operators with strict limits or + // async-CA-heavy fleets should keep at 25 or lower. + // + // Values ≤ 0 fall back to 1 (sequential) — fail-safe rather than + // panicking on semaphore.NewWeighted(0) semantics. + // + // Closes the #9 acquisition-readiness blocker from the 2026-05-01 + // issuer coverage audit. Pre-fix the per-tick fan-out had no cap, + // so a 5k-cert sweep launched 5k in-flight HTTP calls to upstream + // CAs and tripped DigiCert/Entrust/Sectigo rate limits. + // + // Setting: CERTCTL_RENEWAL_CONCURRENCY environment variable. + RenewalConcurrency int + + // AgentHealthCheckInterval is how often the scheduler checks agent heartbeats. + // Default: 2 minutes. Minimum: 1 second. Marks agents offline if no recent heartbeat. + // Setting: CERTCTL_SCHEDULER_AGENT_HEALTH_CHECK_INTERVAL environment variable. + AgentHealthCheckInterval time.Duration + + // NotificationProcessInterval is how often the scheduler processes pending notifications. + // Default: 1 minute. Minimum: 1 second. Sends notifications to Slack, Teams, PagerDuty, etc. + // Setting: CERTCTL_SCHEDULER_NOTIFICATION_PROCESS_INTERVAL environment variable. + NotificationProcessInterval time.Duration + + // NotificationRetryInterval is how often the scheduler retries failed + // notifications whose retry_count is below the service-layer 5-attempt + // DLQ budget. Default: 2 minutes. Minimum: 1 second. Mirrors the I-001 + // RetryInterval knob: transitions eligible Failed notifications whose + // next_retry_at has arrived back to Pending so the notification processor + // picks them up on its next tick (closes coverage gap I-005 — HEAD had + // no retry path for transient SMTP/webhook failures and notifications + // stayed Failed forever). + // Setting: CERTCTL_NOTIFICATION_RETRY_INTERVAL environment variable. + NotificationRetryInterval time.Duration + + // RetryInterval is how often the scheduler retries failed jobs whose Attempts + // counter is below MaxAttempts. Default: 5 minutes. Minimum: 1 second. + // Transitions eligible Failed jobs back to Pending so the job processor can + // pick them up again (closes coverage gap I-001 — JobService.RetryFailedJobs + // had no caller prior to this loop being wired). + // Setting: CERTCTL_SCHEDULER_RETRY_INTERVAL environment variable. + RetryInterval time.Duration + + // JobTimeoutInterval is how often the reaper loop sweeps AwaitingCSR and + // AwaitingApproval jobs for TTL expiration. Default: 10 minutes. Minimum: 1 + // second. Timed-out jobs are transitioned to Failed with a descriptive error + // message; I-001's retry loop then auto-promotes eligible Failed jobs back + // to Pending (closes coverage gap I-003). + // Setting: CERTCTL_JOB_TIMEOUT_INTERVAL environment variable. + JobTimeoutInterval time.Duration + + // AwaitingCSRTimeout is the maximum age an AwaitingCSR job can remain in + // that state before the reaper transitions it to Failed. Default: 24 hours. + // An agent that hasn't submitted a CSR within this window is presumed + // unreachable. Minimum: 1 second. + // Setting: CERTCTL_JOB_AWAITING_CSR_TIMEOUT environment variable. + AwaitingCSRTimeout time.Duration + + // AwaitingApprovalTimeout is the maximum age an AwaitingApproval job can + // remain in that state before the reaper transitions it to Failed. Default: + // 168 hours (7 days). Reviewers who haven't approved within this window + // force the renewal to fail loudly rather than silently stall. Minimum: 1 + // 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 + + // CRLGenerationInterval is how often the scheduler pre-generates + // CRLs into the crl_cache table. The /.well-known/pki/crl/{issuer_id} + // HTTP endpoint reads from this cache instead of regenerating per + // request. Default: 1 hour. + // Setting: CERTCTL_CRL_GENERATION_INTERVAL environment variable. + // Bundle CRL/OCSP-Responder Phase 3. + CRLGenerationInterval time.Duration + + // OCSPRateLimitPerIPMin is the per-source-IP cap on OCSP requests + // per minute. Defaults to 1000 (production hardening II Phase 3 + // frozen decision 0.5). Zero disables the limit. + // Setting: CERTCTL_OCSP_RATE_LIMIT_PER_IP_MIN environment variable. + OCSPRateLimitPerIPMin int + + // CertExportRateLimitPerActorHr is the per-actor cap on cert-export + // requests per hour. Defaults to 50 (production hardening II Phase + // 3 frozen decision 0.6). Zero disables the limit. + // Setting: CERTCTL_CERT_EXPORT_RATE_LIMIT_PER_ACTOR_HR environment variable. + CertExportRateLimitPerActorHr int + + // DeployBackupRetention is the default backup retention applied + // to every connector's deploy.Plan when the per-target config + // doesn't override. Defaults to 3 (deploy-hardening I frozen + // decision 0.2). Set to -1 to disable backups entirely (rollback + // becomes impossible — documented foot-gun). + // Setting: CERTCTL_DEPLOY_BACKUP_RETENTION environment variable. + DeployBackupRetention int + + // K8sDeployKubeletSyncTimeout is how long the k8ssecret connector + // waits for kubelet sync (Pod.Status.ContainerStatuses indicating + // the new Secret has been mounted) after a Secret update before + // timing out the post-deploy verify. Defaults to 60s. + // Setting: CERTCTL_K8S_DEPLOY_KUBELET_SYNC_TIMEOUT environment variable. + // Deploy-hardening I Phase 9. + K8sDeployKubeletSyncTimeout time.Duration +} + +// LogConfig contains logging configuration. +type LogConfig struct { + // Level sets the minimum log level for output. + // Valid values: "debug" (verbose), "info" (default), "warn" (warnings), "error" (errors only). + // Setting: CERTCTL_LOG_LEVEL environment variable. Default: "info". + Level string + + // Format sets the output format for logs. + // Valid values: "json" (structured, for parsing), "text" (human-readable). + // Setting: CERTCTL_LOG_FORMAT environment variable. Default: "json". + Format string +} + +// RateLimitConfig contains rate limiting configuration. +// +// Bundle B / Audit M-025 (OWASP ASVS L2 §11.2.1): pre-bundle the rate +// limiter was global (a single token bucket shared across every request); +// post-bundle it is per-key with separate budgets for IP-keyed and +// user-keyed buckets. RPS / BurstSize are PER-KEY budgets. +type RateLimitConfig struct { + // Enabled controls whether rate limiting is enforced on API endpoints. + // Default: true. Set to false to disable rate limits (not recommended for production). + // Setting: CERTCTL_RATE_LIMIT_ENABLED environment variable. + Enabled bool + + // RPS is the target requests per second allowed PER KEY (token bucket + // rate). For unauthenticated callers the key is the source IP; for + // authenticated callers the key is the API-key name (UserKey context + // value populated by NewAuthWithNamedKeys). + // Default: 50. Higher values allow burst throughput; lower values restrict load. + // Setting: CERTCTL_RATE_LIMIT_RPS environment variable. + RPS float64 + + // BurstSize is the maximum number of requests allowed in a single burst. + // Default: 100. Allows clients to exceed RPS briefly when BurstSize tokens available. + // Must be at least as large as RPS. Higher = more lenient burst handling. + // Setting: CERTCTL_RATE_LIMIT_BURST environment variable. + BurstSize int + + // PerUserRPS overrides RPS for authenticated callers. When zero, RPS is + // used for both keying dimensions. Set this higher than RPS to grant + // authenticated clients a more generous budget than anonymous probes. + // Default: 0 (use RPS). + // Setting: CERTCTL_RATE_LIMIT_PER_USER_RPS environment variable. + PerUserRPS float64 + + // PerUserBurstSize overrides BurstSize for authenticated callers. When + // zero, BurstSize is used. Default: 0 (use BurstSize). + // Setting: CERTCTL_RATE_LIMIT_PER_USER_BURST environment variable. + PerUserBurstSize int +} + +// CORSConfig contains CORS configuration. +type CORSConfig struct { + // AllowedOrigins is a list of allowed origins for CORS requests. + // Security default: empty list denies all CORS requests (same-origin only). + // ["*"] allows all origins (development/demo mode only, security risk). + // Specific origins (e.g., ["https://app.example.com"]) whitelist only those origins. + AllowedOrigins []string +} + +// isLoopbackAddr returns true when host is bound to a loopback +// interface only (127.0.0.1, ::1, or "localhost"). Used by the +// HIGH-12 demo-mode startup guard to refuse non-loopback binds when +// CERTCTL_AUTH_TYPE=none is in effect. +// +// "" (unset) AND "0.0.0.0" / "::" / "[::]" return false because those +// surface the listener to every interface — exactly the misconfiguration +// the guard is designed to catch. +// +// Hostnames other than "localhost" return false defensively: a hostname +// could resolve to a non-loopback IP at runtime; we don't perform DNS +// here because the guard runs at startup before any network state is +// available, and we don't want a misconfigured /etc/hosts to silently +// pass the guard. Operators wanting to bind to a non-default loopback +// alias must either use 127.0.0.1 / ::1 directly or set +// CERTCTL_DEMO_MODE_ACK=true. +func isLoopbackAddr(host string) bool { + switch host { + case "": + // Empty / unset host — Go's net/http.Server treats this as + // "all interfaces" (equivalent to 0.0.0.0). Surface it to the + // network → not loopback. + return false + case "0.0.0.0", "::", "[::]": + return false + case "localhost": + return true + } + // Strip a trailing :port if the operator passed a host:port pair + // rather than a bare host (defensive — Server.Host is documented + // as host-only, but be lenient). + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + if ip := net.ParseIP(host); ip != nil { + return ip.IsLoopback() + } + // Hostname that isn't "localhost" — fail closed. + return false +}