fix(docs,code): ARCH-004 + SEC-003-K8S + ARCH-003 — marketing claims now match code truth

Sprint 4 unified-master-audit closure. Three claim-truth-alignment
findings whose README edits land on shared lines, bundled into one
commit.

ARCH-004 — 'full REST API exposed as MCP tools' overclaim:
  Pre-fix the README said 'the full REST API is exposed as MCP
  tools'; the actual MCP coverage is 162 tools / 220 routes
  (~74%). The remaining gap is intentional: protocol-conformance
  endpoints (ACME/SCEP/EST/OCSP/CRL), browser-only auth flow,
  health/ready, and streaming/binary downloads — categories that
  don't fit the request-response JSON tool shape.

  Fix:
    - README L78 qualified to 'the bulk of the REST API surface'
      with explicit numbers + pointer to the new coverage doc.
    - New docs/reference/mcp-coverage.md publishes the exclusion
      categories with rationale + the canonical commands to
      re-derive route + tool counts.
    - New scripts/ci-guards/mcp-coverage-parity.sh fails the build
      if the tool count drops below (routes − exclusions − 40-slack),
      so a future regression that drops 50+ tools surfaces in CI.
      Verified locally: clean at 162 tools / 220 routes / 37
      intentional exclusions.

SEC-003-K8S — Kubernetes Secrets connector is a runtime stub:
  Pre-fix README L67 marketed 'fifteen native target connectors'
  with Kubernetes Secrets in the list, but realK8sClient's CRUD
  methods returned 'real Kubernetes client not implemented' in
  production. Per the audit's option (b) recommendation: downgrade
  marketing + runtime-guard the stub.

  Fix:
    - README L12 + L67: 'fourteen production-ready native deployment-
      target connectors plus Kubernetes Secrets (preview)'.
    - k8ssecret.New() now refuses to construct unless
      CERTCTL_K8SSECRET_PREVIEW_ACK=true is set, mirroring the
      SEC-H3 ACK pattern. NewWithClient path (test injection)
      unchanged.
    - docs/reference/connectors/index.md moves Kubernetes Secrets
      out of the canonical fourteen-target list into a new 'Preview
      connectors' subsection.
    - Regression tests in k8ssecret_test.go pin the new gate
      (rejects without ACK, accepts with ACK, still rejects nil
      config even with ACK).

ARCH-003 — CERTCTL_KEYGEN_MODE=server breaks the blanket claim:
  Pre-fix README L12 + L82 said 'private keys stay on your
  infrastructure' and 'never touch the control plane' as blanket
  promises. Flipping CERTCTL_KEYGEN_MODE=server makes the control
  plane mint keys in process memory — breaking the claim — and
  the only signal was a boot-time slog WARN. An operator who set
  the flag and didn't read logs ran in silent contradiction to the
  marketed posture.

  Fix:
    - config.Validate() refuses to accept KeygenMode='server'
      unless DemoModeAck=true (mirroring SEC-H3). Production
      deploys (the default Mode='agent' path) are unaffected.
    - README L12 + L82 qualified: 'In agent-mode (the default),
      private keys ...; a demo-only CERTCTL_KEYGEN_MODE=server
      flag mints keys server-side, refuses to start without an
      explicit CERTCTL_DEMO_MODE_ACK=true acknowledgement.'
    - Regression tests for the new Validate gate land in
      config_test.go (note: gate tests landed in the ARCH-002
      commit because of contiguous-hunk constraint at the bottom
      of the file).

Closes ARCH-004, SEC-003-K8S, ARCH-003.
This commit is contained in:
shankar0123
2026-05-16 04:55:34 +00:00
parent 34d5200904
commit a0404f2d21
7 changed files with 341 additions and 9 deletions
@@ -13,6 +13,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"regexp"
"time"
@@ -81,13 +82,37 @@ var (
)
// New creates a new Kubernetes Secrets target connector.
// For now, returns a stub error since we're not pulling in k8s.io dependencies.
// The real implementation will use k8s.io/client-go to create a real K8s client.
//
// SEC-003-K8S closure (Sprint 4, 2026-05-16). The production
// k8s.io/client-go integration is not yet wired — realK8sClient's
// CRUD methods at the bottom of this file are stubs that return
// "real Kubernetes client not implemented." Pre-fix, New() would
// happily return a working-looking Connector wrapping the stub
// client; the operator would only see the failure when an actual
// deploy fired against a registered target. Now New() refuses to
// construct the connector unless CERTCTL_K8SSECRET_PREVIEW_ACK=true
// is set, mirroring the SEC-H3 demo-mode ACK pattern. Tests that
// need a working connector (with the in-memory mock client) call
// NewWithClient — that path is unchanged.
//
// README qualifies the connector as preview at line 67; the
// runtime guard here closes the gap where an operator could
// register a k8ssecret target through the GUI / API and silently
// land a non-functional deployment path in their fleet.
func New(cfg *Config, logger *slog.Logger) (*Connector, error) {
if cfg == nil {
return nil, fmt.Errorf("Kubernetes config is required")
}
if os.Getenv("CERTCTL_K8SSECRET_PREVIEW_ACK") != "true" {
return nil, fmt.Errorf(
"k8ssecret connector is preview-only — the production client-go integration ships in a future bundle. " +
"To register a k8ssecret target on this build, set CERTCTL_K8SSECRET_PREVIEW_ACK=true on the server " +
"AND understand that the connector's CRUD calls will return \"real Kubernetes client not implemented\" " +
"until the integration lands. See README.md `Deploy automatically` line and " +
"docs/reference/deployment-model.md for the per-target guarantee matrix")
}
// Stub real K8s client — the actual implementation will use k8s.io/client-go
// For now, return error to guide users to use the agent with proper kubeconfig
client := &realK8sClient{
@@ -644,3 +644,49 @@ func contains(s, substr string) bool {
}
return false
}
// =============================================================================
// SEC-003-K8S closure (Sprint 4, 2026-05-16). The production realK8sClient's
// CRUD methods are stubs that return "real Kubernetes client not implemented."
// Pre-fix, New() returned a working-looking Connector wrapping the stub; the
// operator only saw the failure when a deploy actually fired. Now New()
// refuses to construct unless CERTCTL_K8SSECRET_PREVIEW_ACK=true is set,
// surfacing the preview-only state at registration time.
//
// The NewWithClient path used by tests in this package stays unchanged —
// it injects a mock client and doesn't gate on the env var.
// =============================================================================
func TestNew_RequiresPreviewACK(t *testing.T) {
t.Setenv("CERTCTL_K8SSECRET_PREVIEW_ACK", "")
cfg := &Config{Namespace: "default", SecretName: "tls-cert"}
conn, err := New(cfg, nil)
if err == nil {
t.Fatalf("New() without ACK returned (conn=%v, err=nil); want preview-ACK rejection", conn)
}
if conn != nil {
t.Errorf("New() returned non-nil conn on rejection: %v", conn)
}
}
func TestNew_AcceptsWithPreviewACK(t *testing.T) {
t.Setenv("CERTCTL_K8SSECRET_PREVIEW_ACK", "true")
cfg := &Config{Namespace: "default", SecretName: "tls-cert"}
conn, err := New(cfg, nil)
if err != nil {
t.Fatalf("New() with ACK = %v; want nil error", err)
}
if conn == nil {
t.Fatalf("New() with ACK returned nil connector")
}
}
func TestNew_RejectsNilConfigBeforeACKCheck(t *testing.T) {
// Defense-in-depth: the existing nil-config rejection still
// fires regardless of the ACK env, so an operator who flipped
// the ACK still can't construct with a missing config.
t.Setenv("CERTCTL_K8SSECRET_PREVIEW_ACK", "true")
if _, err := New(nil, nil); err == nil {
t.Fatalf("New(nil, ...) returned nil; want rejection of nil config")
}
}