## Summary Fixes two critical scheduler reliability issues in certctl: ### TICKET-002 (CRITICAL): Scheduler job idempotency - Added atomic.Bool guards to all 6 scheduler loops (renewal, job processor, agent health, notifications, short-lived expiry, network scan) - Uses CompareAndSwap pattern to prevent duplicate execution if previous job is still running - Logs warning when a tick is skipped due to in-flight work - Prevents runaway scheduler duplicates and resource exhaustion ### TICKET-011 (MEDIUM): Graceful shutdown - Added sync.WaitGroup to track in-flight scheduler work - Each job is wrapped in wg.Add(1)/wg.Done() for lifecycle tracking - New WaitForCompletion(timeout) method waits for all in-flight work to complete - Integrates into main.go: after context cancellation, waits up to 30s for jobs to finish before closing DB - Graceful shutdown ensures no work is lost during server restart/termination ## Changes **internal/scheduler/scheduler.go:** - Imports: added "errors", "sync", "sync/atomic" - Scheduler struct: added 6 atomic.Bool fields (one per loop) + sync.WaitGroup - All 6 loop functions: spawn goroutines with wg.Add/Done, check atomic guard on each tick, skip tick if already running - New WaitForCompletion(timeout) method with timeout support - New ErrSchedulerShutdownTimeout error type **cmd/server/main.go:** - After context cancellation and before HTTP shutdown, call sched.WaitForCompletion(30 * time.Second) - Logs "waiting for scheduler to complete in-flight work" and any errors **internal/scheduler/scheduler_test.go (new file):** - Mock services for testing (renewal, job, agent, notification, network scan) - TestSchedulerIdempotencyGuard: verifies slow job doesn't cause duplicate execution - TestWaitForCompletionSuccess: verifies graceful shutdown with adequate timeout - TestWaitForCompletionTimeout: verifies timeout is respected - TestSchedulerMultipleLoopsIdempotency: verifies all 6 loops respect idempotency - TestSchedulerGracefulShutdown: end-to-end graceful shutdown flow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
certctl — Self-Hosted Certificate Lifecycle Platform
timeline
title TLS Certificate Maximum Lifespan (CA/Browser Forum Ballot SC-081v3)
2015 : 5 years
2018 : 825 days
2020 : 398 days
March 2026 : 200 days
March 2027 : 100 days
March 2029 : 47 days
TLS certificate lifespans are shrinking fast. The CA/Browser Forum passed Ballot SC-081v3 unanimously in April 2025, setting a phased reduction: 200 days by March 2026, 100 days by March 2027, and 47 days by March 2029. Organizations managing dozens or hundreds of certificates can no longer rely on spreadsheets, calendar reminders, or manual renewal workflows. The math doesn't work — at 47-day lifespans, a team managing 100 certificates is processing 7+ renewals per week, every week, forever.
certctl is a self-hosted platform that automates the entire certificate lifecycle — from issuance through renewal to deployment — with zero human intervention. It works with any certificate authority, deploys to any server, and keeps private keys on your infrastructure where they belong.
Documentation
| Guide | Description |
|---|---|
| Why certctl? | Competitive positioning — how certctl compares to open-source and enterprise certificate management platforms |
| Concepts | TLS certificates explained from scratch — for beginners who know nothing about certs |
| Quick Start | Get running in 5 minutes — dashboard, API, CLI, discovery, stakeholder demo flow |
| Advanced Demo | Issue a certificate end-to-end with technical deep-dives |
| Architecture | System design, data flow diagrams, security model |
| Feature Inventory | Complete reference of all V2 capabilities, API endpoints, and configuration |
| Connectors | Build custom issuer, target, and notifier connectors |
| Compliance Mapping | SOC 2 Type II, PCI-DSS 4.0, NIST SP 800-57 alignment guides |
Why certctl Exists
Certificate lifecycle tooling today falls into two camps: expensive enterprise platforms (Venafi, Keyfactor, Sectigo) that cost six figures and take months to deploy, or single-purpose tools (cert-manager, certbot) that handle one slice of the problem. If you run a mixed infrastructure — some NGINX, some Apache, a few HAProxy nodes, maybe an F5 — and you need to manage certificates from multiple CAs, there's nothing self-hosted that covers the full lifecycle without vendor lock-in.
certctl fills that gap. It's CA-agnostic — the issuer connector interface means you can plug in any certificate authority: a self-signed local CA for dev, Let's Encrypt via ACME for public certs, Smallstep step-ca for your private PKI, your enterprise ADCS via sub-CA mode, or any custom CA through a shell script adapter. You're never locked to a single CA vendor, and you can run multiple issuers simultaneously for different certificate types.
It's also target-agnostic. Agents deploy certificates to NGINX, Apache, HAProxy, Traefik, and Caddy — all using the same pluggable connector model for any server that accepts cert files. The control plane never initiates outbound connections — agents poll for work, which means certctl works behind firewalls, across network zones, and in air-gapped environments.
For a detailed comparison with CertKit, KeyTalk, and enterprise platforms (Venafi, Keyfactor), see Why certctl?
What It Does
certctl gives you a single pane of glass for every TLS certificate in your organization:
- Web dashboard — full certificate inventory with status, ownership, expiration heatmaps, and bulk operations
- REST API — 97 endpoints under
/api/v1/+/.well-known/est/for complete automation - Agents — generate private keys locally, discover existing certs on disk, submit CSRs (private keys never leave your servers)
- Network scanner — discovers certificates on TLS endpoints across CIDR ranges without requiring agents
- EST server (RFC 7030) — device and WiFi certificate enrollment via industry-standard protocol
- Approval workflows — require human sign-off on renewals before deployment
- Background scheduler — watches expiration dates and triggers renewals automatically, handling constant rotation at 47-day lifespans without human involvement
For the full capability breakdown — revocation infrastructure, policy engine, observability, EST enrollment, and more — see the Feature Inventory.
Supported Integrations
Certificate Issuers
| Issuer | Status | Type |
|---|---|---|
| Local CA (self-signed + sub-CA) | Implemented | GenericCA |
| ACME v2 (Let's Encrypt, Sectigo) | Implemented (HTTP-01 + DNS-01 + DNS-PERSIST-01) | ACME |
| ACME EAB (ZeroSSL, Google Trust) | Implemented (auto-fetch EAB from ZeroSSL) | ACME |
| step-ca | Implemented | StepCA |
| OpenSSL / Custom CA | Implemented | OpenSSL |
| Vault PKI | Future | — |
| DigiCert | Future | — |
Note: ADCS integration is handled via the Local CA's sub-CA mode — certctl operates as a subordinate CA with its signing certificate issued by ADCS. Any CA with a shell-accessible signing interface can be integrated today via the OpenSSL/Custom CA connector.
Deployment Targets
| Target | Status | Type |
|---|---|---|
| NGINX | Implemented | NGINX |
| Apache httpd | Implemented | Apache |
| HAProxy | Implemented | HAProxy |
| Traefik | Implemented | Traefik |
| Caddy | Implemented | Caddy |
| F5 BIG-IP | Interface only | F5 |
| Microsoft IIS | Interface only | IIS |
Notifiers
| Notifier | Status | Type |
|---|---|---|
| Email (SMTP) | Implemented | Email |
| Webhooks | Implemented | Webhook |
| Slack | Implemented | Slack |
| Microsoft Teams | Implemented | Teams |
| PagerDuty | Implemented | PagerDuty |
| OpsGenie | Implemented | OpsGenie |
All connectors are pluggable — build your own by implementing the connector interface.
Screenshots
Quick Start
Docker Pull
docker pull shankar0123.docker.scarf.sh/certctl-server
docker pull shankar0123.docker.scarf.sh/certctl-agent
Docker Compose (Recommended)
git clone https://github.com/shankar0123/certctl.git
cd certctl
docker compose -f deploy/docker-compose.yml up -d --build
Wait ~30 seconds, then open http://localhost:8443 in your browser.
The dashboard comes pre-loaded with 15 demo certificates, 5 agents, policy rules, audit events, and notifications — a realistic snapshot of a certificate inventory so you can explore immediately.
Verify the API:
curl http://localhost:8443/health
# {"status":"healthy"}
curl -s http://localhost:8443/api/v1/certificates | jq '.total'
# 15
Manual Build
# Prerequisites: Go 1.25+, PostgreSQL 16+
go mod download
make build
# Set up database
export CERTCTL_DATABASE_URL="postgres://certctl:certctl@localhost:5432/certctl?sslmode=disable"
export CERTCTL_AUTH_TYPE=none
make migrate-up
# Start server
./bin/server
# Start agent (separate terminal)
export CERTCTL_SERVER_URL=http://localhost:8443
export CERTCTL_API_KEY=change-me-in-production
export CERTCTL_AGENT_NAME=local-agent
export CERTCTL_AGENT_ID=agent-local-01
./bin/agent --agent-id=agent-local-01
Architecture
Control plane (Go 1.25 net/http) → PostgreSQL 16 (21 tables, TEXT primary keys) → Agents (key generation, CSR submission, cert deployment). Background scheduler runs 6 loops: renewal checks (1h), job processing (30s), agent health (2m), notifications (1m), short-lived cert expiry (30s), network scanning (6h). See Architecture Guide for full system diagrams and data flow.
Key Design Decisions
- Private keys isolated from the control plane. Agents generate ECDSA P-256 keys locally and submit CSRs (public key only). The server signs the CSR and returns the certificate — private keys never touch the control plane. Server-side keygen is available via
CERTCTL_KEYGEN_MODE=serverfor demo/development only. - TEXT primary keys, not UUIDs. IDs are human-readable prefixed strings (
mc-api-prod,t-platform,o-alice) so you can identify resource types at a glance in logs and queries. - Handler → Service → Repository layering. Handlers define their own service interfaces for clean dependency inversion. No global service singletons.
- Idempotent migrations. All schema uses
IF NOT EXISTSand seed data usesON CONFLICT (id) DO NOTHING, safe for repeated execution.
PostgreSQL 16 with 21 tables covering certificates, versions, policies, issuers, targets, agents, jobs, teams, owners, profiles, agent groups, revocations, discovery, network scans, and audit events. See the Architecture Guide for the full schema.
Configuration
All environment variables use the CERTCTL_ prefix. Key settings:
| Variable | Default | Description |
|---|---|---|
CERTCTL_DATABASE_URL |
postgres://localhost/certctl |
PostgreSQL connection string |
CERTCTL_AUTH_TYPE |
api-key |
Auth mode: api-key, jwt, or none |
CERTCTL_AUTH_SECRET |
— | Required for api-key and jwt auth types |
CERTCTL_KEYGEN_MODE |
agent |
Key generation: agent (production) or server (demo only) |
CERTCTL_SERVER_PORT |
8080 |
Server listen port |
CERTCTL_ACME_DIRECTORY_URL |
— | ACME directory URL (e.g., Let's Encrypt) |
CERTCTL_ACME_EMAIL |
— | Contact email for ACME account registration |
Agent settings:
| Variable | Default | Description |
|---|---|---|
CERTCTL_SERVER_URL |
http://localhost:8080 |
Control plane URL |
CERTCTL_API_KEY |
— | Agent API key |
CERTCTL_AGENT_ID |
— | Registered agent ID (required) |
CERTCTL_KEY_DIR |
/var/lib/certctl/keys |
Private key storage directory |
CERTCTL_DISCOVERY_DIRS |
— | Directories to scan for existing certs (comma-separated) |
For the full configuration reference — including ACME DNS challenges, sub-CA mode, step-ca, OpenSSL/Custom CA, EST enrollment, network scanning, notification connectors (Slack, Teams, PagerDuty, OpsGenie), scheduler intervals, CORS, and rate limiting — see the Feature Inventory. Docker Compose overrides for the demo stack are in deploy/docker-compose.yml.
Development
# Install dev tools (golangci-lint, migrate CLI, air)
make install-tools
# Run tests
make test
# Run with coverage
make test-coverage
# Lint
make lint
# Format
make fmt
Docker Compose
make docker-up # Start stack (server + postgres + agent)
make docker-down # Stop stack
make docker-logs-server # Server logs
make docker-logs-agent # Agent logs
make docker-clean # Stop + remove volumes
Security
Private Key Management
- Agent keygen mode (default): Agents generate ECDSA P-256 keys locally and store them with 0600 permissions in
CERTCTL_KEY_DIR(default/var/lib/certctl/keys). Only the CSR (public key) is sent to the control plane. Private keys never leave agent infrastructure. - Server keygen mode (demo only): Set
CERTCTL_KEYGEN_MODE=serverfor development/demo with Local CA. The control plane generates RSA-2048 keys server-side. A log warning is emitted at startup.
Authentication
- Agent-to-server: API key (registered at agent creation)
- API key and JWT auth types supported;
nonefor demo/development - Auth type and secret configured via
CERTCTL_AUTH_TYPEandCERTCTL_AUTH_SECRET
Audit Trail
- Immutable append-only log in PostgreSQL (
audit_eventstable) - Every lifecycle action attributed to an actor with timestamp and resource reference
- No update or delete operations on audit records
- Every API call recorded to audit trail with method, path, actor, SHA-256 body hash, response status, and latency
API Overview
97 endpoints under /api/v1/ + /.well-known/est/, all returning JSON. List endpoints support pagination, sparse field selection (?fields=), sort (?sort=-notAfter), time-range filters, and cursor-based pagination. Full request/response schemas in the OpenAPI 3.1 spec.
Key Endpoints
# Certificate lifecycle
GET /api/v1/certificates List (filter, sort, cursor, sparse fields)
POST /api/v1/certificates/{id}/renew Trigger renewal → 202 Accepted
POST /api/v1/certificates/{id}/revoke Revoke with RFC 5280 reason code
GET /api/v1/crl/{issuer_id} DER-encoded X.509 CRL
GET /api/v1/ocsp/{issuer_id}/{serial} OCSP responder (good/revoked/unknown)
# Agent operations
POST /api/v1/agents/{id}/csr Submit CSR for issuance
GET /api/v1/agents/{id}/work Poll for pending deployment jobs
POST /api/v1/agents/{id}/discoveries Submit certificate discovery scan results
# Discovery & network scanning
GET /api/v1/discovered-certificates List discovered certs (?agent_id, ?status)
POST /api/v1/discovered-certificates/{id}/claim Link to managed cert
POST /api/v1/network-scan-targets/{id}/scan Trigger immediate TLS scan
# Jobs & approval
POST /api/v1/jobs/{id}/approve Approve interactive renewal
POST /api/v1/jobs/{id}/reject Reject interactive renewal
# Post-deployment verification
POST /api/v1/jobs/{id}/verify Submit TLS verification result
GET /api/v1/jobs/{id}/verification Get verification status
# Observability
GET /api/v1/metrics/prometheus Prometheus exposition format
GET /api/v1/stats/summary Dashboard summary
# EST enrollment (RFC 7030)
POST /.well-known/est/simpleenroll Device certificate enrollment
GET /.well-known/est/cacerts CA certificate chain (PKCS#7)
Full CRUD is available for certificates, agents, issuers, targets, teams, owners, policies, profiles, agent groups, notifications, and audit events. See the OpenAPI spec or Feature Inventory for the complete endpoint reference.
CLI
# Install
go install github.com/shankar0123/certctl/cmd/cli@latest
# Configure
export CERTCTL_SERVER_URL=http://localhost:8443
export CERTCTL_API_KEY=your-api-key
# Certificate commands
certctl-cli certs list # List all certificates
certctl-cli certs get mc-api-prod # Get certificate details
certctl-cli certs renew mc-api-prod # Trigger renewal
certctl-cli certs revoke mc-api-prod --reason keyCompromise
# Agent and job commands
certctl-cli agents list # List registered agents
certctl-cli jobs list # List jobs
certctl-cli jobs cancel job-123 # Cancel a pending job
# Operations
certctl-cli status # Server health + summary stats
certctl-cli import certs.pem # Bulk import from PEM file
# Output formats
certctl-cli certs list --format json # JSON output (default: table)
MCP Server (AI Integration)
certctl ships a standalone MCP (Model Context Protocol) server that exposes all 78 API endpoints as tools for AI assistants — Claude, Cursor, Windsurf, OpenClaw, VS Code Copilot, and any MCP-compatible client.
# Install
go install github.com/shankar0123/certctl/cmd/mcp-server@latest
# Configure
export CERTCTL_SERVER_URL=http://localhost:8443
export CERTCTL_API_KEY=your-api-key
# Run (stdio transport — add to your AI client config)
mcp-server
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"certctl": {
"command": "mcp-server",
"env": {
"CERTCTL_SERVER_URL": "http://localhost:8443",
"CERTCTL_API_KEY": "your-api-key"
}
}
}
}
Roadmap
V1 (v1.0.0)
Core lifecycle management — Local CA + ACME v2 issuers, NGINX target connector, agent-side key generation, API auth + rate limiting, React dashboard, CI pipeline with coverage gates, Docker images on GHCR.
V2: Operational Maturity
18 milestones complete, 950+ tests. See the Feature Inventory for details on every capability.
What shipped (all ✅):
-
Issuers — Sub-CA mode (enterprise root chains), ACME DNS-01 + DNS-PERSIST-01 (wildcard certs, any DNS provider), step-ca (native /sign API), OpenSSL/Custom CA (script-based signing)
-
Revocation — RFC 5280 reason codes, DER-encoded X.509 CRL, embedded OCSP responder, short-lived cert exemption
-
Profiles + Ownership — certificate profiles (key types, max TTL, crypto constraints), ownership tracking (owners + teams), dynamic agent groups, interactive renewal approval
-
GUI Operations — bulk renew/revoke/reassign, deployment timeline, inline policy editor, target wizard, audit export (CSV/JSON), short-lived credentials view
-
Discovery — filesystem scanning (PEM/DER) + network TLS scanning (CIDR ranges), triage workflow (claim/dismiss), network scan target management
-
Observability — Prometheus + JSON metrics, 5 stats API endpoints, dashboard charts (heatmap, trends, distribution), agent fleet overview, structured logging
-
EST Server (RFC 7030) — device/WiFi certificate enrollment, PKCS#7 wire format, configurable issuer + profile binding
-
MCP Server — 78 API operations as AI tools for Claude, Cursor, and any MCP-compatible client
-
CLI — 12 subcommands (list/get/renew/revoke certs, agents, jobs, import, status), JSON/table output
-
Notifications — Slack, Microsoft Teams, PagerDuty, OpsGenie connectors
-
API Enhancements — sparse fields, sort, time-range filters, cursor pagination, immutable API audit logging
-
Compliance Mapping — SOC 2 Type II, PCI-DSS 4.0, NIST SP 800-57 alignment guides
-
Post-Deployment TLS Verification — agent-side TLS probe confirms the target is serving the correct certificate by SHA-256 fingerprint match
-
Traefik + Caddy Targets — Traefik (file provider, auto-reload) and Caddy (Admin API hot-reload or file-based)
Coming next:
- Certificate Export (v2.1.x) — single-cert download in PFX/PKCS12, DER, and PEM formats
- S/MIME Support (v2.2.x) — profile EKU constraints for S/MIME (emailProtection), code signing, and custom EKUs
V3: certctl Pro
Team access controls, identity provider integration, enterprise deployment targets, compliance and risk scoring, advanced fleet operations, event-driven architecture, advanced search, real-time operational views, and premium CA integrations.
V4+: Cloud, Scale & Passive Discovery
Passive network discovery (TLS listener), Kubernetes integration (cert-manager external issuer, Secrets target), cloud infrastructure targets (AWS ALB/ACM, Azure Key Vault), extended CA support (Vault PKI, Google CAS, EJBCA), and platform-scale features (Terraform provider, multi-tenancy, HSM support).
License
Certctl is licensed under the Business Source License 1.1. The source code is publicly available and free to use, modify, and self-host. The one restriction: you may not offer certctl as a managed/hosted certificate management service to third parties.















