feat(ssh,wincertstore,javakeystore,k8ssecret): explicit ValidateOnly + leverage existing connectors

Phase 9 of the deploy-hardening I master bundle. The four
non-file-server connectors get real ValidateOnly probes that
operators use to preview a deploy without touching the live cert.
Existing DeployCertificate paths already have explicit backup +
rollback semantics (SCP backup / WinCertStore Get-ChildItem
snapshot / keytool snapshot / K8s atomic API).

SSH (validate_only.go):
- Probes via SSHClient.Connect. Confirms agent reachability +
  credentials. Cheap (no remote command runs); released cleanly
  via defer Close.
- A true SCP dry-run requires a no-commit upload (SCP doesn't
  have one). V2 ships the auth probe as the load-bearing check.
- 3 new tests in validate_only_test.go.

WinCertStore (validate_only.go):
- Probes via PowerShell `Get-ChildItem -Path Cert:\<loc>\<store>`
  using the configured StoreLocation + StoreName (defaults
  LocalMachine\My).
- Confirms agent has Windows + the IIS module + the right ACLs.
- 4 new tests including default-store-path verification.

JavaKeystore (validate_only.go):
- Probes via `keytool -list -keystore <path> -storepass <pass>`
  using the configured KeystorePath / KeystorePassword and
  KeytoolPath (default "keytool").
- Confirms keystore exists, password is correct, JRE is on PATH.
- 4 new tests covering succeeds / fails / no-path-sentinel /
  nil-executor-sentinel.

K8s Secret (validate_only.go):
- Probes via K8sClient.GetSecret on the configured Namespace +
  SecretName. Returns nil on success or "not found" (the
  CreateSecret path on Deploy will handle it). Other errors
  (forbidden/unreachable) surface as wrapped.
- 4 new tests covering succeeds / RBAC-error wrapped /
  no-config-sentinel / nil-client-sentinel.

Smoke test connectorsAtPhase3 list shrunk from 7 to 3 entries
(ssh + wincertstore + javakeystore + k8ssecret removed). Only
caddy (file-mode) + envoy + traefik remain — those three
genuinely have no validate-with-target command available.

Race detector clean across all 13 connectors. golangci-lint
v2.11.4 clean.

Phase 10 next: DeployCounters + Prometheus exposer mirroring the
production-hardening-II OCSP counter pattern.
This commit is contained in:
shankar0123
2026-04-30 15:22:17 +00:00
parent 36d79cd1ff
commit 9f41b58b2f
9 changed files with 317 additions and 41 deletions
@@ -27,13 +27,13 @@ import (
// f5 removed Phase 8 — real ValidateOnly implementation now in validate_only.go.
// haproxy removed Phase 6 — real ValidateOnly implementation now in haproxy.go.
// iis removed Phase 8 — real ValidateOnly implementation now in validate_only.go.
"github.com/shankar0123/certctl/internal/connector/target/javakeystore"
"github.com/shankar0123/certctl/internal/connector/target/k8ssecret"
// javakeystore removed Phase 9 — real ValidateOnly implementation now in validate_only.go.
// k8ssecret removed Phase 9 — real ValidateOnly implementation now in validate_only.go.
// nginx removed Phase 4 — real ValidateOnly implementation now in nginx.go.
// postfix removed Phase 7 — real ValidateOnly implementation now in postfix.go.
"github.com/shankar0123/certctl/internal/connector/target/ssh"
// ssh removed Phase 9 — real ValidateOnly implementation now in validate_only.go.
"github.com/shankar0123/certctl/internal/connector/target/traefik"
"github.com/shankar0123/certctl/internal/connector/target/wincertstore"
// wincertstore removed Phase 9 — real ValidateOnly implementation now in validate_only.go.
)
// connectorsAtPhase3 is the canonical list of connectors that, as
@@ -74,20 +74,20 @@ var connectorsAtPhase3 = []struct {
// f5 removed Phase 8 — Authenticate-probe real impl.
// haproxy removed Phase 6 — `haproxy -c -f` real impl.
// iis removed Phase 8 — Get-WebSite probe real impl.
{"javakeystore", func() target.Connector { return &javakeystore.Connector{} }},
{"k8ssecret", func() target.Connector { return &k8ssecret.Connector{} }},
// javakeystore removed Phase 9 — `keytool -list` real impl.
// k8ssecret removed Phase 9 — GetSecret RBAC probe real impl.
// nginx removed Phase 4 — `nginx -t` real impl.
// postfix removed Phase 7 — `postfix check` / `doveconf -n` real impl.
{"ssh", func() target.Connector { return &ssh.Connector{} }},
// ssh removed Phase 9 — Connect probe real impl.
// traefik: no validate-with-target command exists; always sentinel.
{"traefik", func() target.Connector { return &traefik.Connector{} }},
{"wincertstore", func() target.Connector { return &wincertstore.Connector{} }},
// wincertstore removed Phase 9 — `Get-ChildItem Cert:\` probe.
}
func TestEveryConnectorDefaultsToSentinel(t *testing.T) {
// Expected list size shrinks as Phases 4-9 land their real
// ValidateOnly implementations. Phase 4 removed nginx.
const expectedAtCurrentPhase = 7
const expectedAtCurrentPhase = 3
if len(connectorsAtPhase3) != expectedAtCurrentPhase {
t.Fatalf("connectors-at-phase list = %d entries, want %d (drift in the 13-connector inventory)", len(connectorsAtPhase3), expectedAtCurrentPhase)
}