feat: M11a — certificate profiles, crypto policy enforcement, short-lived cert expiry

Add certificate profiles as named enrollment templates that control allowed
key algorithms, max TTL, permitted EKUs, required SAN patterns, and optional
SPIFFE URI SANs. CSR submissions are validated against profile rules at
signing time (key type + minimum size). Short-lived certs (TTL < 1 hour)
auto-expire via a new scheduler loop — expiry acts as revocation, no
CRL/OCSP needed.

New files:
- Migration 000003: certificate_profiles table, FK columns on
  managed_certificates/renewal_policies, key metadata on certificate_versions
- domain/profile.go: CertificateProfile + KeyAlgorithmRule structs
- repository/postgres/profile.go: full CRUD with JSONB marshaling
- service/profile.go: ProfileService with validation + audit logging
- service/crypto_validation.go: CSR-against-profile validation (RSA/ECDSA/Ed25519)
- handler/profiles.go: 5 HTTP endpoints under /api/v1/profiles
- web/src/pages/ProfilesPage.tsx: profiles management page

Modified:
- renewal.go: CSR validation in CompleteAgentCSRRenewal, ExpireShortLivedCertificates
- scheduler.go: 30s short-lived expiry check loop
- certificate.go (repo): nullable profile FK, key metadata on versions
- main.go: profile repo/service/handler wiring, 8-param NewRenewalService
- router.go: 12-param RegisterHandlers with profile routes
- seed_demo.sql: 4 demo profiles (standard, mtls, short-lived, high-security)
- Frontend: types, API client, routing, sidebar nav

Tests: 40 new tests across handler (15), service (13), crypto validation (12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-20 20:39:49 -04:00
parent 7450fcfb07
commit a579a84c7f
27 changed files with 2399 additions and 71 deletions
@@ -0,0 +1,13 @@
-- Rollback: remove certificate profiles and associated columns
ALTER TABLE certificate_versions DROP COLUMN IF EXISTS key_algorithm;
ALTER TABLE certificate_versions DROP COLUMN IF EXISTS key_size;
ALTER TABLE renewal_policies DROP COLUMN IF EXISTS certificate_profile_id;
DROP INDEX IF EXISTS idx_managed_certificates_profile_id;
ALTER TABLE managed_certificates DROP COLUMN IF EXISTS certificate_profile_id;
DROP INDEX IF EXISTS idx_certificate_profiles_name;
DROP INDEX IF EXISTS idx_certificate_profiles_enabled;
DROP TABLE IF EXISTS certificate_profiles;
@@ -0,0 +1,53 @@
-- M11a: Certificate Profiles + Crypto Foundation
-- Named enrollment profiles defining allowed key types, max TTL, required SANs,
-- permitted EKUs, and optional SPIFFE URI SAN patterns.
-- Table: certificate_profiles
CREATE TABLE IF NOT EXISTS certificate_profiles (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT DEFAULT '',
-- Crypto policy: which key algorithms and minimum sizes are allowed
-- Example: [{"algorithm": "ECDSA", "min_size": 256}, {"algorithm": "RSA", "min_size": 2048}]
allowed_key_algorithms JSONB NOT NULL DEFAULT '[{"algorithm": "ECDSA", "min_size": 256}, {"algorithm": "RSA", "min_size": 2048}]',
-- Maximum certificate TTL in seconds (0 = no limit, uses issuer default)
-- Short-lived: 300 (5 min), 3600 (1 hour). Standard: 7776000 (90 days), 4060800 (47 days)
max_ttl_seconds INT NOT NULL DEFAULT 0,
-- Permitted Extended Key Usages
-- Example: ["serverAuth", "clientAuth"]
allowed_ekus JSONB NOT NULL DEFAULT '["serverAuth"]',
-- Required SAN patterns (regexes that issued certs must match)
-- Example: [".*\\.example\\.com$", ".*\\.internal\\.example\\.com$"]
required_san_patterns JSONB NOT NULL DEFAULT '[]',
-- Optional SPIFFE URI SAN pattern for workload identity
-- Example: "spiffe://example.com/workload/*"
-- Empty string means no SPIFFE SAN will be minted
spiffe_uri_pattern VARCHAR(512) DEFAULT '',
-- Whether this profile allows short-lived certs (TTL < 1 hour)
-- When true, expired certs under this profile skip CRL/OCSP (expiry = revocation)
allow_short_lived BOOLEAN NOT NULL DEFAULT false,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_certificate_profiles_name ON certificate_profiles(name);
CREATE INDEX IF NOT EXISTS idx_certificate_profiles_enabled ON certificate_profiles(enabled);
-- Add certificate_profile_id FK to managed_certificates (nullable for backward compat)
ALTER TABLE managed_certificates ADD COLUMN IF NOT EXISTS certificate_profile_id TEXT REFERENCES certificate_profiles(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_managed_certificates_profile_id ON managed_certificates(certificate_profile_id);
-- Add certificate_profile_id FK to renewal_policies (nullable — profile scoping on policies)
ALTER TABLE renewal_policies ADD COLUMN IF NOT EXISTS certificate_profile_id TEXT REFERENCES certificate_profiles(id) ON DELETE SET NULL;
-- Add key metadata to certificate_versions for audit / compliance
ALTER TABLE certificate_versions ADD COLUMN IF NOT EXISTS key_algorithm VARCHAR(50) DEFAULT '';
ALTER TABLE certificate_versions ADD COLUMN IF NOT EXISTS key_size INT DEFAULT 0;
+36
View File
@@ -53,6 +53,42 @@ INSERT INTO deployment_targets (id, name, type, agent_id, config, enabled, creat
('tgt-nginx-data', 'NGINX Data Services', 'nginx', 'ag-data-prod', '{"cert_path": "/etc/nginx/ssl/cert.pem", "key_path": "/etc/nginx/ssl/key.pem", "reload_command": "nginx -s reload"}', true, NOW(), NOW())
ON CONFLICT (id) DO NOTHING;
-- Certificate Profiles
INSERT INTO certificate_profiles (id, name, description, allowed_key_algorithms, max_ttl_seconds, allowed_ekus, required_san_patterns, spiffe_uri_pattern, allow_short_lived, enabled, created_at, updated_at) VALUES
('prof-standard-tls', 'Standard TLS',
'Default profile for web-facing TLS certificates. Requires ECDSA P-256+ or RSA 2048+.',
'[{"algorithm": "ECDSA", "min_size": 256}, {"algorithm": "RSA", "min_size": 2048}]'::jsonb,
7776000, -- 90 days
'["serverAuth"]'::jsonb,
'[]'::jsonb,
'', false, true, NOW(), NOW()),
('prof-internal-mtls', 'Internal mTLS',
'Mutual TLS profile for internal service-to-service communication.',
'[{"algorithm": "ECDSA", "min_size": 256}]'::jsonb,
2592000, -- 30 days
'["serverAuth", "clientAuth"]'::jsonb,
'[".*\\.internal\\.example\\.com$"]'::jsonb,
'', false, true, NOW(), NOW()),
('prof-short-lived', 'Short-Lived Credential',
'Ephemeral certificates for CI/CD pipelines and container workloads. TTL under 1 hour, expiry = revocation.',
'[{"algorithm": "ECDSA", "min_size": 256}]'::jsonb,
300, -- 5 minutes
'["serverAuth", "clientAuth"]'::jsonb,
'[]'::jsonb,
'spiffe://example.com/workload/*',
true, true, NOW(), NOW()),
('prof-high-security', 'High Security',
'For PCI-DSS and compliance-sensitive workloads. RSA 4096+ or ECDSA P-384+ only.',
'[{"algorithm": "ECDSA", "min_size": 384}, {"algorithm": "RSA", "min_size": 4096}]'::jsonb,
4060800, -- 47 days (Ballot SC-081v3 target)
'["serverAuth"]'::jsonb,
'[".*\\.example\\.com$"]'::jsonb,
'', false, true, NOW(), NOW())
ON CONFLICT (id) DO NOTHING;
-- Managed Certificates — varied statuses and expiry dates for realistic dashboard
INSERT INTO managed_certificates (id, name, common_name, sans, environment, owner_id, team_id, issuer_id, renewal_policy_id, status, expires_at, tags, last_renewal_at, last_deployment_at, created_at, updated_at) VALUES
-- Active, healthy certs