fix: end-to-end certificate lifecycle bugs + integration test environment

Fixes 12 production bugs preventing the full issuance→deployment flow
from working with ACME (Pebble/Let's Encrypt) and step-ca issuers:

ACME connector (acme.go):
- Save orderURI before WaitOrder overwrites it (Go crypto/acme bug)
- Add CreateOrderCert fallback via WaitOrder+FetchCert
- Remove defer-reset in ValidateConfig that caused nil pointer panic
- Add Insecure TLS option for self-signed ACME servers (Pebble)

step-ca connector (stepca.go, jwe.go):
- Real JWE provisioner key loading + decryption (was using ephemeral keys)
- Fix JWT audience (/1.0/sign), sha claim (key fingerprint), kid header
- Custom root CA trust via RootCertPath config
- Remove hardcoded 90-day validity default (let step-ca decide)

NGINX target connector (nginx.go):
- Use sh -c for validate/reload commands (shell interpretation)
- Use filepath.Dir instead of fragile string slicing
- Add private key file writing (agent-mode keys were never deployed)
- Make chain_path write conditional

Server/service layer:
- TriggerRenewalWithActor now creates actual Job records (was no-op)
- createDeploymentJobs falls back to DB query when cert.TargetIDs empty
- ProcessPendingJobs skips agent-routed deployment jobs
- Agent cert pickup path parsing: len(parts)<4 → len(parts)<3
- Health/ready/auth-info endpoints bypass auth middleware
- Write timeout 15s→120s for ACME issuance
- Cert fingerprint computed on CSR submission

Integration test environment (deploy/test/):
- 10-phase test script covering Local CA, ACME, step-ca, revocation,
  discovery, renewal, and API spot checks
- Docker Compose with 7 containers (server, agent, postgres, nginx,
  pebble, challtestsrv, step-ca) on isolated network
- TLS verification checks SAN (not just Subject CN) for modern CA compat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-04-02 17:02:20 -04:00
parent 2238f28610
commit b059ec930f
19 changed files with 2102 additions and 84 deletions
+130
View File
@@ -0,0 +1,130 @@
-- =============================================================================
-- certctl Test Environment — Seed Data
-- =============================================================================
--
-- Pre-populates the database with the minimum objects needed to test the full
-- certificate lifecycle against real CA backends (Pebble, step-ca, Local CA).
--
-- Load order (handled by Docker entrypoint filename sorting):
-- 001_schema.sql → ... → 008_verification.sql → 010_seed.sql → 015_seed_test.sql
--
-- All IDs use a "test-" prefix so they're easy to spot in the dashboard.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- Team
-- ---------------------------------------------------------------------------
INSERT INTO teams (id, name, description)
VALUES (
'team-test-ops',
'Test Operations',
'Operations team for certctl testing environment'
) ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- Owner (references team)
-- ---------------------------------------------------------------------------
INSERT INTO owners (id, name, email, team_id)
VALUES (
'owner-test-admin',
'Test Admin',
'admin@certctl-test.local',
'team-test-ops'
) ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- Agent — must exist before the agent binary sends its first heartbeat
-- ---------------------------------------------------------------------------
-- The agent binary (certctl-agent container) connects with:
-- CERTCTL_AGENT_ID=agent-test-01
-- CERTCTL_AGENT_NAME=test-agent-01
-- The heartbeat handler does a GET by ID — if the agent doesn't exist, it 404s.
-- api_key_hash is SHA-256 of "test-agent-key-2026" (not used for auth, just stored).
INSERT INTO agents (id, name, hostname, status, registered_at, api_key_hash, os, architecture, ip_address, version)
VALUES (
'agent-test-01',
'test-agent-01',
'certctl-test-agent',
'online',
NOW(),
'cad819dee454889f686d678f691e5084e58ba149762eae2fda4d0bd2abaceefa',
'linux',
'amd64',
'10.30.50.8',
'test'
) ON CONFLICT (id) DO NOTHING;
-- The network scanner uses "server-scanner" as a virtual agent.
-- It gets auto-created by the server code, but seed it here to avoid races.
INSERT INTO agents (id, name, hostname, status, registered_at, api_key_hash)
VALUES (
'server-scanner',
'server-scanner',
'certctl-server',
'online',
NOW(),
'no-key'
) ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- Issuers — one row per CA backend in the test environment
-- ---------------------------------------------------------------------------
-- These are metadata records the dashboard reads. The actual CA connections
-- are configured via env vars on the server container.
-- Local CA (self-signed, always available)
INSERT INTO issuers (id, name, type, config, enabled)
VALUES (
'iss-local',
'Local CA (Self-Signed)',
'local',
'{"mode": "self-signed", "description": "Built-in self-signed CA for testing"}'::jsonb,
true
) ON CONFLICT (id) DO NOTHING;
-- ACME via Pebble (simulates Let''s Encrypt)
INSERT INTO issuers (id, name, type, config, enabled)
VALUES (
'iss-acme-staging',
'ACME (Pebble Test CA)',
'acme',
'{"directory_url": "https://pebble:14000/dir", "email": "test@certctl.dev", "challenge_type": "http-01", "description": "Pebble ACME test server simulating Lets Encrypt"}'::jsonb,
true
) ON CONFLICT (id) DO NOTHING;
-- step-ca (Smallstep private CA)
INSERT INTO issuers (id, name, type, config, enabled)
VALUES (
'iss-stepca',
'step-ca (Private CA)',
'stepca',
'{"url": "https://step-ca:9000", "provisioner": "admin", "description": "Smallstep private CA with JWK provisioner"}'::jsonb,
true
) ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- Certificate Profile — TLS server certs, 90-day max
-- ---------------------------------------------------------------------------
INSERT INTO certificate_profiles (id, name, description, max_ttl_seconds, allowed_ekus, allowed_key_algorithms)
VALUES (
'prof-test-tls',
'Test TLS Server',
'Standard TLS server certificate profile for testing',
7776000, -- 90 days
'["serverAuth"]'::jsonb,
'[{"algorithm": "ECDSA", "min_size": 256}, {"algorithm": "RSA", "min_size": 2048}]'::jsonb
) ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- Deployment Target — NGINX (references agent-test-01)
-- ---------------------------------------------------------------------------
-- The agent deploys certs to NGINX via the shared nginx_certs volume.
INSERT INTO deployment_targets (id, name, type, agent_id, config, enabled)
VALUES (
'target-test-nginx',
'Test NGINX',
'NGINX',
'agent-test-01',
'{"cert_path": "/nginx-certs/cert.pem", "key_path": "/nginx-certs/key.pem", "chain_path": "/nginx-certs/chain.pem", "reload_command": "true", "validate_command": "true"}'::jsonb,
true
) ON CONFLICT (id) DO NOTHING;