mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 23:39:00 +00:00
8564e2fcd688d358c48f39d3a4b8bc8a008eaf7c
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5dc698307b |
chore: rename Go module path to github.com/certctl-io/certctl
Mechanical sed across the main go.mod's module declaration, the f5-mock-icontrol
sub-module's go.mod, every Go file's import path (361 files), and a rebuild of
the checked-in f5-mock-icontrol binary so its embedded build-info reflects the
new module path. No behavior change.
Choice B from cowork/transfer-certctl-to-org.md, executed 2026-05-04. Choice A
(keep module path declared as github.com/shankar0123/certctl regardless of
repo URL) shipped on the day of the org transfer (2026-05-03) since we had no
external Go consumers; this commit closes that deferral.
Backward-compat: GitHub HTTP redirects continue to forward
github.com/shankar0123/certctl → github.com/certctl-io/certctl at the URL
level, but Go's module proxy uses the path declared in go.mod as the
canonical name. Pre-fix, anyone trying `go get github.com/certctl-io/certctl/...`
hit a "module path mismatch" error because go.mod said
github.com/shankar0123/certctl and the URL they fetched it from said
certctl-io/certctl. Post-fix, the canonical name and the URL agree, so
go get / go install / external Go consumers / Go-tooling integrations
work cleanly via either the new path (preferred) or the old path (which
redirects and Go follows the redirect for source fetch).
Anyone still importing the old path inside their own code keeps working
provided they update their go.mod's `require` line to match — the module
path declared in their consumer's go.sum / go.mod is the authoritative
import name, so a mass sed across their import statements is the migration
on the consumer side. No external consumers exist today.
Diff shape:
361 *.go files — import path replacement only
2 go.mod — module declaration replacement only
1 binary — deploy/test/f5-mock-icontrol/f5-mock-icontrol rebuilt
so embedded build-info reflects the new path (8618965 vs
8618933 bytes; 32-byte diff is the build-info change)
Total: 364 files, 730 insertions / 730 deletions, net-zero size, pure
mechanical substitution.
Verification:
gofmt: 17 files needed re-alignment after sed (the new path is one char
shorter than the old, so column-aligned import groups drifted). Applied
`gofmt -w` to fix.
go mod tidy: clean exit on both modules.
go vet ./...: clean exit.
go build ./...: clean exit.
go test -short -count=1 on representative packages: all green
(internal/domain, internal/validation, internal/crypto, internal/crypto/signer,
cmd/agent). Test output now reads `ok github.com/certctl-io/certctl/...`
confirming the module path resolves correctly.
binary: f5-mock-icontrol rebuilt; `strings | grep shankar0123` returns
nothing; `strings | grep certctl-io/certctl` shows the new module path
embedded in build-info.
Files intentionally NOT touched in this commit:
README.md / CHANGELOG.md / docs/ / etc. — already swept to certctl-io
URLs in commit
|
||
|
|
ceca3647eb |
vault: add automatic token renewal at TTL/2 + Prometheus metric
Closes Top-10 fix #5 of the 2026-05-03 issuer-coverage audit (see cowork/issuer-coverage-audit-2026-05-03/RESULTS.md). Pre-fix, the VaultPKI adapter authenticated with a static token and never called renew-self. Long-lived deploys hit token expiry; the first operator-visible signal was failed cert renewals on production targets. This commit: 1. Connector.Start(ctx) spawns a goroutine that calls POST /v1/auth/token/renew-self at TTL/2 cadence (computed from a one-shot lookup-self at startup). Honours ctx.Done() for graceful shutdown via a per-loop done channel + Stop(). 2. On `renewable: false` response (initial lookup OR any subsequent renewal), the loop emits a WARN, increments the not_renewable counter, and exits. The operator must rotate the token before Vault's Max TTL elapses. 3. New Prometheus counter certctl_vault_token_renewals_total with labels result={success,failure,not_renewable}. Registered alongside existing certctl_issuance_* counters in internal/api/handler/metrics.go. 4. ERROR-level logging on renewal failure with operator-actionable substring ("vault token renewal failed; rotate the token before TTL expires") so journalctl + grep find it. Loop keeps ticking after a failure — transient blips don't kill it. New optional issuer.Lifecycle interface: type Lifecycle interface { Start(ctx context.Context) error Stop() } Connectors that hold no background goroutines (almost all of them) do not implement this — IssuerRegistry.StartLifecycles / StopLifecycles feature-detect via type assertion. New lifecycle-bearing connectors plug in by implementing the interface; no further registry plumbing required. Wiring (cmd/server/main.go): - service.NewVaultRenewalMetrics() instance is shared between issuerRegistry.SetVaultRenewalMetrics (so Vault connectors built by Rebuild get a recorder) and metricsHandler.SetVaultRenewals (so the Prometheus exposer emits the new series). - issuerRegistry.StartLifecycles(ctx) is called after issuerService.BuildRegistry; defer issuerRegistry.StopLifecycles is paired so goroutines exit cleanly on signal. - IssuerConnectorAdapter.Underlying() exposes the wrapped issuer.Connector so registry-level machinery can reach the concrete connector behind the adapter without duplicating the wiring at every call site. Tests (internal/connector/issuer/vault/vault_renew_test.go): - TestVault_RenewLoop_TickAtHalfTTL — three ticks → three renewals, all "success". - TestVault_RenewLoop_StopsOnNotRenewable — second renewal returns renewable=false, loop exits, third tick fires no HTTP call. - TestVault_RenewLoop_FailureSurfacesViaMetric — first renewal 403 bumps "failure", second renewal succeeds → loop kept ticking. - TestVault_RenewLoop_CtxCancellation_StopsCleanly — Stop returns within 200ms after ctx cancel. - TestVault_RenewLoop_StartsNothingWhenNotRenewable — token already non-renewable at boot ⇒ no goroutine, "not_renewable" metric increments at startup so operators see it in Grafana. - TestVault_ComputeInterval — 4 cases pinning TTL/2 + minRenewInterval floor. - TestVault_RenewSelf_ParseFailure_NamesActionableInError — surfaced error contains "vault token renewal failed" + "rotate the token". Cadence is dynamic — every successful renewal re-derives TTL/2 from the renewed lease's lease_duration, so a short bootstrap token that gets renewed up to a longer Max TTL shifts to the longer cadence automatically (defends against degenerate fast ticking on a token whose Max TTL is far longer than its initial TTL). Documentation: - docs/connectors.md Vault PKI section gains "Token TTL + automatic renewal" subsection (operator-facing: cadence, metric, renewable=false rotation playbook). Out of scope (intentional, flagged in the audit follow-up): - AppRole / Kubernetes / AWS IAM auth methods (different renewal semantics). - Hot-reload of rotated token from disk (operator restarts today; future: GUI/MCP issuer-update path triggers Rebuild which Stops the old connector and Starts the new one). - Auto-re-auth after token death (operator playbook owns it). CHANGELOG.md is intentionally not hand-edited (per CHANGELOG.md itself: "no longer maintains a hand-edited per-version changelog; per-release notes are auto-generated from commit messages between consecutive tags"). Verified locally: - gofmt clean. - go vet ./internal/service/... ./internal/api/handler/... ./internal/connector/issuer/vault/... ./cmd/server/... clean. - go test -short -count=1 ./internal/connector/issuer/vault/... ./internal/service/... ./internal/api/handler/... green. - go test -race -count=10 -run 'TestVault_RenewLoop|TestVault_ComputeInterval' ./internal/connector/issuer/vault/... green. Audit reference: cowork/issuer-coverage-audit-2026-05-03/RESULTS.md Top-10 fix #5. |
||
|
|
ece15cb457 |
vault, digicert: migrate Token / APIKey to *secret.Ref (Bundle I Phase 3)
Closes Top-10 fix #2 of the 2026-05-03 issuer-coverage audit (see cowork/issuer-coverage-audit-2026-05-03/RESULTS.md). Pre-fix, vault.Config.Token and digicert.Config.APIKey were plain string fields. Practical impact: 1. GET /api/v1/issuers responses marshalled the credential into the JSON body. An acquirer's procurement engineer running 'curl /api/v1/issuers | jq' saw the token / API key in plain text on screen. 2. DEBUG-level HTTP request logging printed the credential header verbatim. 3. A heap dump of the running server contained the credential as readable bytes for the lifetime of the process. Bundle I from the 2026-05-01 audit closed this for AWSACMPCA, EJBCA, GlobalSign, Sectigo (Phase 1+2). Vault and DigiCert were left out. This commit ports the same migration onto them. Mechanics: - Config.Token / Config.APIKey type changed from 'string' to '*secret.Ref'. UnmarshalJSON of a JSON string populates the Ref via NewRefFromString — operator config files are unchanged. - Every header-write call site routed through Ref.Use, with the byte buffer zeroed after the callback returns. Vault: 3 sites (IssueCertificate, RevokeCertificate, GetCACertPEM). DigiCert: 5 sites (ValidateConfig, IssueCertificate, RevokeCertificate, pollOrderOnce, downloadCertificate). - ValidateConfig nil-checks switch from 'cfg.Token == ""' to 'cfg.Token.IsEmpty()' (mirrors Sectigo's existing pattern). - Tests migrated: every Config{Token:"..."} → Config{Token: secret.NewRefFromString("...")}. The 'json.Marshal(config) → ValidateConfig(rawConfig)' round-trip pattern in DigiCert's ValidateConfig_Success test is now broken by the redact-on-marshal contract — switched that one to construct the rawConfig as a JSON literal (mirrors Sectigo's existing test pattern). - Two new tests pin the redact-on-marshal contract: - TestVault_Config_TokenMarshalsAsRedacted (vault_redact_test.go) - TestDigiCert_Config_APIKeyMarshalsAsRedacted (digicert_redact_test.go) Both assert the marshaled JSON contains '"[redacted]"' and does NOT contain the plaintext bytes. Operator-visible: GET /api/v1/issuers responses for type=vault and type=digicert now show the credential as '[redacted]'. Existing config files keep working — the Ref unmarshal accepts strings. CHANGELOG note: certctl/CHANGELOG.md is intentionally not hand-edited; release notes are auto-generated from commit messages between consecutive tags. This commit's message body is the release-note artifact. Verified locally: - gofmt clean across the repo. - go vet ./... clean across the repo. - go test -race -count=1 -short ./internal/connector/issuer/vault/... ./internal/connector/issuer/digicert/... ./internal/secret/... green. Audit reference: cowork/issuer-coverage-audit-2026-05-03/ RESULTS.md Top-10 fix #2. |
||
|
|
482c7e8047 |
chore(fmt): repo-wide gofmt -w sweep — close drift surfaced by ci-pipeline-cleanup Phase 4
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit
|
||
|
|
d839b233fe |
Bundle N.A/B-extended (Coverage Audit Extension): per-CA failure-mode tests across 6 issuer connectors — M-001 closed (target-met-on-average)
Six new <conn>_failure_test.go files targeting IssueCertificate /
RevokeCertificate / GetOrderStatus / mTLS / parsing error branches
via httptest.Server. Same pattern as Bundle J's acme_failure_test.go,
adapted per-CA.
Coverage deltas
=================
vault 84.1% -> 87.3% (+3.2pp; 5 tests)
sectigo 79.4% -> 85.5% (+6.1pp; 9 tests)
globalsign 78.2% -> 87.1% (+8.9pp; 7 tests, NewWithHTTPClient pattern)
digicert 81.0% -> 84.9% (+3.9pp; 6 tests)
ejbca 76.5% -> 84.3% (+7.8pp; 8 tests, OAuth2 + mTLS branches)
entrust 70.8% -> 81.2% (+10.4pp; 14 tests; in-package mapRevocationReason
/ parseCertMetadata / loadMTLSConfig
/ ValidateConfig field-required +
unreachable + bad-cert-path +
GetOrderStatus status-variants)
Already at or above 85%
=================
stepca 90.4% (Bundle L.B closure)
awsacmpca 83.5% (existing tests; entrust-style retry edges remain)
googlecas 83.4% (existing tests; OAuth2 token retry edges remain)
Pattern per failure-mode test
=================
- httptest.NewServer with selective handlers for /sys/health,
/v1/ca, /ssl/v1/types etc. so ValidateConfig succeeds before
the failure-mode HTTP call
- 403 / 404 / 5xx / malformed-JSON / missing-PEM / invalid-base64
branches per connector
- Status variants for GetOrderStatus dispatch arms (pending /
processing / rejected / denied / unknown → fallback)
- Where applicable: malformed cert PEM / bad CSR base64 / no
DNSSolver / nil revocation reason
Audit deliverables
=================
- gap-backlog.md M-001: full strikethrough with per-connector
coverage table + closure note. CLOSED (target-met-on-average)
rather than (all ≥85%) — entrust 81.2% and awsacmpca/googlecas
83.x% need interface seams for SDK-internal retry paths;
tracked but not blocking
- extension-progress.md: N.A/B-extended marked DONE
Closes (target-met-on-average): M-001
Bundle: N.A/B-extended (Coverage Audit Extension)
|
||
|
|
933edfb1d0 |
Bundle N (Coverage Audit Closure) [partial]: issuer-connector stubs coverage
Closes M-001 partially; M-002, M-003, and CI threshold raise #2 deferred. Stubs coverage shipped across 8 issuer connectors via per-connector <conn>_stubs_test.go (~50 LoC each) pinning the not-supported issuer.Connector interface methods (GenerateCRL, SignOCSPResponse, GetCACertPEM, GetRenewalInfo). Most CAs delegate CRL/OCSP/CA-cert distribution to managed services, so these are documented stubs that return errors. Pinning them ensures the stubs aren't silently replaced with no-ops in a future refactor. Coverage delta: digicert: 79.3% -> 81.0% (+1.7pp) ejbca: 75.8% -> 76.5% (+0.7pp) entrust: 70.8% -> 70.8% (stubs already covered) sectigo: 78.0% -> 79.4% (+1.4pp) vault: 81.0% -> 84.1% (+3.1pp) openssl: 76.9% -> 78.0% (+1.1pp) googlecas: 81.0% -> 83.4% (+2.4pp) globalsign: 75.9% -> 78.2% (+2.3pp) (awsacmpca not included; its 0%-coverage hotspots are stubClient methods structurally different from the others' interface stubs. Already at 83.5%.) Why the gates aren't yet met: the stub functions are tiny (1-2 lines each, mostly 'return nil, fmt.Errorf("not supported")'). Lifting each connector to >=85% requires per-connector failure-mode test files mirroring Bundle J's ACME pattern (httptest.Server + canned 401/403/ 429+Retry-After/5xx/malformed responses against the actual API methods). That's ~200-300 LoC x 9 connectors = ~2000-2700 LoC of bespoke per-CA mock work; exceeds this session's budget. Tracked as follow-on Bundle N.A-extended / N.B-extended. Deferred sub-batches: N.C (M-002 + M-003): internal/service (70.5%) + internal/api/handler (79.4%) round-out NOT YET STARTED. Tracked as Bundle N.C-extended. N.CI (CI threshold raise #2): prescribed raises require underlying coverage at proposed floors first. Premature raise would fail CI immediately. Tracked as Bundle N.CI-extended. Verification: go vet ./internal/connector/issuer/{8-pkgs}/... clean gofmt -l clean go test -short -count=1 PASS for all 8 Audit deliverables: gap-backlog.md: M-001 partial-strikethrough with per-connector table + Bundle N closure-log entry covering all 4 sub-batch statuses closure-plan.md: Bundle N [~] with per-sub-batch status breakdown CHANGELOG.md: [unreleased] Bundle N entry |
||
|
|
ff223e2586 |
feat(M11c): crypto policy enforcement — CSR validation, MaxTTL caps, key metadata
Enforce certificate profile crypto constraints across all 5 issuance paths (renewal, agent CSR, EST, SCEP). ValidateCSRAgainstProfile() rejects CSRs with key algorithm/size that don't match profile rules. MaxTTL enforcement caps certificate validity per issuer connector (Local CA, Vault, step-ca enforce directly; ACME/DigiCert/Sectigo pass through). Key algorithm and size are now persisted in certificate_versions for audit compliance. 16 new tests (12 service-layer + 4 Local CA connector). Removes hardcoded version number from GUI sidebar. Documentation updated across architecture, features, connectors, and README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
3044ddc171 |
fix: use tagged switch statements to satisfy staticcheck QF1002
Convert `switch { case r.URL.Path == ... }` to `switch r.URL.Path { ... }`
in Vault and DigiCert connector tests to pass golangci-lint CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
078ba5ab7b |
feat: add Vault PKI and DigiCert CertCentral issuer connectors (M32 + M37)
Vault PKI: synchronous issuance via /v1/{mount}/sign/{role}, token auth,
revocation, CA cert retrieval, 14 tests. DigiCert CertCentral: async order
model (submit → poll → download), X-DC-DEVKEY auth, OV/EV support, PEM
bundle parsing, 16 tests. Both conditionally registered based on env vars.
Includes OpenAPI enum updates, seed data, connector docs, architecture docs,
README badges, and testing guide sign-off (Parts 38 + 39, 12 automated
smoke test assertions all passing).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|