Files
certctl/migrations/000001_initial_schema.up.sql
T
Shankar 17a3e4a4b1 Implement M3: expiration threshold alerting with dedup and status transitions
- Add alert_thresholds_days JSONB column to renewal_policies (default [30,14,7,0])
- Add RenewalPolicy.AlertThresholdsDays field + EffectiveAlertThresholds() helper
- Add RenewalPolicyRepository interface + postgres implementation
- Rewrite CheckExpiringCertificates with per-policy threshold alerting
- Add SendThresholdAlert + HasThresholdNotification for deduplication via [threshold:N] tags
- Add Type and MessageLike filters to NotificationFilter + postgres query support
- Auto-transition certs to Expiring (>0 days) or Expired (<=0 days) status
- Record expiration_alert_sent audit events per threshold crossing
- Fix .gitignore: allow SQL migration files, scope server/agent build artifact rules
- Track previously untracked cmd/ and migrations/ directories
- Update docs (README, architecture, demo-advanced) for threshold alerting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 00:03:43 -04:00

224 lines
8.6 KiB
SQL

-- Create initial schema for certificate control plane
-- IDs are TEXT to support application-generated prefixed IDs (e.g., "team-123", "cert-456")
-- Table: teams
CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_teams_name ON teams(name);
-- Table: owners
CREATE TABLE IF NOT EXISTS owners (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_owners_email ON owners(email);
CREATE INDEX IF NOT EXISTS idx_owners_team_id ON owners(team_id);
-- Table: renewal_policies
CREATE TABLE IF NOT EXISTS renewal_policies (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
renewal_window_days INT NOT NULL,
auto_renew BOOLEAN NOT NULL DEFAULT true,
max_retries INT NOT NULL DEFAULT 3,
retry_interval_minutes INT NOT NULL DEFAULT 60,
alert_thresholds_days JSONB NOT NULL DEFAULT '[30, 14, 7, 0]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_renewal_policies_name ON renewal_policies(name);
-- Table: issuers
CREATE TABLE IF NOT EXISTS issuers (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
type VARCHAR(255) NOT NULL,
config JSONB,
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_issuers_name ON issuers(name);
CREATE INDEX IF NOT EXISTS idx_issuers_enabled ON issuers(enabled);
-- Table: agents
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
hostname VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'offline',
last_heartbeat_at TIMESTAMPTZ,
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
api_key_hash VARCHAR(255) NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
CREATE INDEX IF NOT EXISTS idx_agents_hostname ON agents(hostname);
CREATE INDEX IF NOT EXISTS idx_agents_last_heartbeat_at ON agents(last_heartbeat_at);
-- Table: managed_certificates
CREATE TABLE IF NOT EXISTS managed_certificates (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
common_name VARCHAR(255) NOT NULL,
sans TEXT[] DEFAULT ARRAY[]::TEXT[],
environment VARCHAR(50),
owner_id TEXT NOT NULL REFERENCES owners(id) ON DELETE RESTRICT,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
issuer_id TEXT NOT NULL REFERENCES issuers(id) ON DELETE RESTRICT,
renewal_policy_id TEXT NOT NULL REFERENCES renewal_policies(id) ON DELETE RESTRICT,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
expires_at TIMESTAMPTZ,
tags JSONB NOT NULL DEFAULT '{}',
last_renewal_at TIMESTAMPTZ,
last_deployment_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_status ON managed_certificates(status);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_expires_at ON managed_certificates(expires_at);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_owner_id ON managed_certificates(owner_id);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_team_id ON managed_certificates(team_id);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_issuer_id ON managed_certificates(issuer_id);
CREATE INDEX IF NOT EXISTS idx_managed_certificates_name ON managed_certificates(name);
-- Table: deployment_targets
CREATE TABLE IF NOT EXISTS deployment_targets (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
config JSONB,
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_deployment_targets_agent_id ON deployment_targets(agent_id);
CREATE INDEX IF NOT EXISTS idx_deployment_targets_enabled ON deployment_targets(enabled);
CREATE INDEX IF NOT EXISTS idx_deployment_targets_name ON deployment_targets(name);
-- Table: certificate_target_mappings
CREATE TABLE IF NOT EXISTS certificate_target_mappings (
certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE,
target_id TEXT NOT NULL REFERENCES deployment_targets(id) ON DELETE CASCADE,
PRIMARY KEY (certificate_id, target_id)
);
CREATE INDEX IF NOT EXISTS idx_certificate_target_mappings_target_id ON certificate_target_mappings(target_id);
-- Table: certificate_versions
CREATE TABLE IF NOT EXISTS certificate_versions (
id TEXT PRIMARY KEY,
certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE,
serial_number VARCHAR(255) NOT NULL,
not_before TIMESTAMPTZ NOT NULL,
not_after TIMESTAMPTZ NOT NULL,
fingerprint_sha256 VARCHAR(255) NOT NULL UNIQUE,
pem_chain TEXT NOT NULL,
csr_pem TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_certificate_versions_certificate_id ON certificate_versions(certificate_id);
CREATE INDEX IF NOT EXISTS idx_certificate_versions_fingerprint ON certificate_versions(fingerprint_sha256);
-- Table: jobs
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
type VARCHAR(255) NOT NULL,
certificate_id TEXT REFERENCES managed_certificates(id) ON DELETE CASCADE,
target_id TEXT REFERENCES deployment_targets(id) ON DELETE SET NULL,
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 3,
last_error TEXT,
deployment_result JSONB,
scheduled_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
CREATE INDEX IF NOT EXISTS idx_jobs_certificate_id ON jobs(certificate_id);
CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at);
CREATE INDEX IF NOT EXISTS idx_jobs_agent_id ON jobs(agent_id);
-- Table: policy_rules
CREATE TABLE IF NOT EXISTS policy_rules (
id TEXT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
type VARCHAR(255) NOT NULL,
config JSONB NOT NULL,
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_policy_rules_name ON policy_rules(name);
CREATE INDEX IF NOT EXISTS idx_policy_rules_enabled ON policy_rules(enabled);
-- Table: policy_violations
CREATE TABLE IF NOT EXISTS policy_violations (
id TEXT PRIMARY KEY,
certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE,
rule_id TEXT NOT NULL REFERENCES policy_rules(id) ON DELETE CASCADE,
message TEXT NOT NULL,
severity VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_policy_violations_certificate_id ON policy_violations(certificate_id);
CREATE INDEX IF NOT EXISTS idx_policy_violations_rule_id ON policy_violations(rule_id);
CREATE INDEX IF NOT EXISTS idx_policy_violations_severity ON policy_violations(severity);
-- Table: audit_events
CREATE TABLE IF NOT EXISTS audit_events (
id TEXT PRIMARY KEY,
actor VARCHAR(255) NOT NULL,
actor_type VARCHAR(50) NOT NULL,
action VARCHAR(255) NOT NULL,
resource_type VARCHAR(255) NOT NULL,
resource_id VARCHAR(255) NOT NULL,
details JSONB,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_audit_events_resource_type_id ON audit_events(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_audit_events_actor ON audit_events(actor);
CREATE INDEX IF NOT EXISTS idx_audit_events_timestamp ON audit_events(timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action);
-- Table: notification_events
CREATE TABLE IF NOT EXISTS notification_events (
id TEXT PRIMARY KEY,
type VARCHAR(255) NOT NULL,
certificate_id TEXT REFERENCES managed_certificates(id) ON DELETE CASCADE,
channel VARCHAR(255) NOT NULL,
recipient VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
sent_at TIMESTAMPTZ,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
error TEXT
);
CREATE INDEX IF NOT EXISTS idx_notification_events_certificate_id ON notification_events(certificate_id);
CREATE INDEX IF NOT EXISTS idx_notification_events_status ON notification_events(status);
CREATE INDEX IF NOT EXISTS idx_notification_events_type ON notification_events(type);