All service-layer Create methods (team, owner, target, issuer,
certificate) were unconditionally overwriting user-provided IDs with
auto-generated ones and leaving CreatedAt/UpdatedAt as zero values.
This caused three user-visible bugs:
- POST /api/v1/teams with {"id": "t-demo"} returned a generated ID
like "team-1773601137949154216" instead of "t-demo"
- POST /api/v1/owners referencing the user-provided team_id failed
with Internal Server Error (FK constraint on non-existent generated ID)
- created_at/updated_at came back as "0001-01-01T00:00:00Z"
Fix: all 9 affected Create methods (both context-aware and handler
interface variants) now check if ID is empty before generating, and
set timestamps to time.Now() if zero-valued. Follows the existing
correct pattern in policy.go CreateRule/CreatePolicy.
Also removes two stale temp files (audit.go.* and issuer.go.*) that
were accidentally committed to the repo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
certctl — Open-Source Certificate Control Plane
A self-hosted certificate lifecycle platform. Track, renew, and deploy TLS certificates across your infrastructure with a web dashboard, REST API, and agent-based architecture where private keys never leave your servers.
What It Does
certctl gives you a single pane of glass for every TLS certificate in your organization. The web dashboard shows your full certificate inventory — what's healthy, what's expiring, what's already expired, and who owns each one. The REST API (40+ endpoints) lets you automate everything. Agents deployed on your infrastructure generate private keys locally and submit CSRs — private keys never leave your servers.
flowchart LR
subgraph "Control Plane"
API["REST API + Dashboard\n:8443"]
PG[("PostgreSQL")]
end
subgraph "Your Infrastructure"
A1["Agent"] --> T1["NGINX"]
A2["Agent"] --> T2["F5 BIG-IP"]
A3["Agent"] --> T3["IIS"]
end
API --> PG
A1 & A2 & A3 -->|"CSR + status\n(no private keys)"| API
API -->|"Signed certs"| A1 & A2 & A3
API -->|"Issue/Renew"| CA["Certificate Authorities\nLocal CA · ACME"]
Quick Start
Docker Compose (Recommended)
git clone https://github.com/shankar0123/certctl.git
cd certctl
docker compose -f deploy/docker-compose.yml up -d
Wait ~30 seconds, then open http://localhost:8443 in your browser.
The dashboard comes pre-loaded with 14 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'
# 14
Manual Build
# Prerequisites: Go 1.22+, 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
Documentation
| Guide | Description |
|---|---|
| Concepts | TLS certificates explained from scratch — for beginners who know nothing about certs |
| Quick Start | Get running in 5 minutes with accurate API examples |
| Demo Walkthrough | 5-7 minute guided stakeholder presentation |
| Advanced Demo | Issue a certificate end-to-end with technical deep-dives |
| Architecture | System design, data flow diagrams, security model |
| Connectors | Build custom issuer, target, and notifier connectors |
Architecture
flowchart TB
subgraph "Control Plane (certctl-server)"
DASH["Web Dashboard\nReact SPA"]
API["REST API\nGo 1.22 net/http"]
SVC["Service Layer"]
REPO["Repository Layer\ndatabase/sql + lib/pq"]
SCHED["Scheduler\nRenewal · Jobs · Health · Notifications"]
end
subgraph "Data Store"
PG[("PostgreSQL 16\n14 tables · TEXT primary keys")]
end
subgraph "Agents"
AG["certctl-agent\nKey generation · CSR · Deployment"]
end
DASH --> API
API --> SVC --> REPO --> PG
SCHED --> SVC
AG -->|"Heartbeat + CSR"| API
API -->|"Cert + Chain"| AG
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.
Database Schema
| Table | Purpose |
|---|---|
managed_certificates |
Certificate records with metadata, status, expiry, tags |
certificate_versions |
Historical versions with PEM chains and CSRs |
renewal_policies |
Renewal window, auto-renew settings, retry config, alert thresholds |
issuers |
CA configurations (Local CA, ACME, etc.) |
deployment_targets |
Target systems (NGINX, F5, IIS) with agent assignments |
agents |
Registered agents with heartbeat tracking |
jobs |
Issuance, renewal, deployment, and validation jobs |
teams |
Organizational groups for certificate ownership |
owners |
Individual owners with email for notifications |
policy_rules |
Enforcement rules (allowed issuers, environments, metadata) |
policy_violations |
Flagged non-compliance with severity levels |
audit_events |
Immutable action log (append-only, no update/delete) |
notification_events |
Email and webhook notification records |
certificate_target_mappings |
Many-to-many cert ↔ target relationships |
Configuration
All server environment variables use the CERTCTL_ prefix:
| Variable | Default | Description |
|---|---|---|
CERTCTL_SERVER_HOST |
127.0.0.1 |
Server bind address |
CERTCTL_SERVER_PORT |
8080 |
Server listen port |
CERTCTL_DATABASE_URL |
postgres://localhost/certctl |
PostgreSQL connection string |
CERTCTL_DATABASE_MAX_CONNS |
25 |
Connection pool size |
CERTCTL_LOG_LEVEL |
info |
Log level: debug, info, warn, error |
CERTCTL_LOG_FORMAT |
json |
Log format: json or text |
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 mode: agent (production) or server (demo only) |
CERTCTL_ACME_DIRECTORY_URL |
— | ACME directory URL (e.g., Let's Encrypt staging) |
CERTCTL_ACME_EMAIL |
— | Contact email for ACME account registration |
Agent environment variables:
| Variable | Default | Description |
|---|---|---|
CERTCTL_SERVER_URL |
http://localhost:8080 |
Control plane URL |
CERTCTL_API_KEY |
— | Agent API key |
CERTCTL_AGENT_NAME |
certctl-agent |
Agent display name |
CERTCTL_AGENT_ID |
— | Registered agent ID (required) |
CERTCTL_KEY_DIR |
/var/lib/certctl/keys |
Directory for storing private keys (agent keygen mode) |
Docker Compose overrides these for the demo stack (see deploy/docker-compose.yml): port 8443, auth type none, database pointing to the postgres container.
API Overview
All endpoints are under /api/v1/ and return JSON. List endpoints support pagination (?page=1&per_page=50).
Certificates
GET /api/v1/certificates List (filter: status, environment, owner_id, team_id)
POST /api/v1/certificates Create
GET /api/v1/certificates/{id} Get
PUT /api/v1/certificates/{id} Update
DELETE /api/v1/certificates/{id} Archive (soft delete)
GET /api/v1/certificates/{id}/versions Version history
POST /api/v1/certificates/{id}/renew Trigger renewal → 202 Accepted
POST /api/v1/certificates/{id}/deploy Trigger deployment → 202 Accepted
Agents
GET /api/v1/agents List
POST /api/v1/agents Register
GET /api/v1/agents/{id} Get
POST /api/v1/agents/{id}/heartbeat Record heartbeat
POST /api/v1/agents/{id}/csr Submit CSR for issuance
GET /api/v1/agents/{id}/certificates/{certId} Retrieve signed certificate
GET /api/v1/agents/{id}/work Poll for pending deployment jobs
POST /api/v1/agents/{id}/jobs/{jobId}/status Report job completion/failure
Infrastructure
GET /api/v1/issuers List issuers
POST /api/v1/issuers Create
GET /api/v1/issuers/{id} Get
POST /api/v1/issuers/{id}/test Test connectivity
GET /api/v1/targets List deployment targets
POST /api/v1/targets Create
GET /api/v1/targets/{id} Get
Organization
GET /api/v1/teams List teams
POST /api/v1/teams Create
GET /api/v1/owners List owners
POST /api/v1/owners Create
Operations
GET /api/v1/jobs List (filter: status, type)
GET /api/v1/jobs/{id} Get
POST /api/v1/jobs/{id}/cancel Cancel
GET /api/v1/policies List policy rules
POST /api/v1/policies Create
GET /api/v1/policies/{id}/violations List violations for rule
GET /api/v1/audit Query audit trail
GET /api/v1/notifications List notifications
Health
GET /health Server health check
GET /ready Readiness check
Supported Integrations
Certificate Issuers
| Issuer | Status | Type |
|---|---|---|
| Local CA (self-signed) | Implemented | GenericCA |
| ACME v2 (Let's Encrypt, Sectigo) | Implemented (HTTP-01) | ACME |
| Vault PKI | Planned | — |
| DigiCert | Planned | — |
Deployment Targets
| Target | Status | Type |
|---|---|---|
| NGINX | Implemented | NGINX |
| F5 BIG-IP | Implemented | F5 |
| Microsoft IIS | Implemented | IIS |
| Kubernetes Secrets | Planned | — |
Notifiers
| Notifier | Status | Type |
|---|---|---|
| Email (SMTP) | Implemented | Email |
| Webhooks | Implemented | Webhook |
| Slack | Planned | — |
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 action attributed to an actor with timestamp and resource reference
- No update or delete operations on audit records
Roadmap
V1 (feature-complete → v1.0.0 tag pending)
All nine development milestones (M1–M9) are complete. The backend covers the full certificate lifecycle: Local CA and ACME v2 issuers, NGINX/F5/IIS target connectors, threshold-based expiration alerting, agent-side ECDSA P-256 key generation, API auth with rate limiting, and a React dashboard with 11 views wired to the real API. The CI pipeline runs build, vet, lint, test with coverage gates (service layer 30%+, handler layer 50%+), and frontend checks on every push. 170+ tests across service, handler, integration, and connector layers.
Remaining before the v1.0.0 tag: dashboard screenshots in README, tagged Docker images published, final error-handling audit to confirm no panics or unhandled error paths.
V2: Operational Maturity
- V2.0: Operational Workflows — renewal approval UI, bulk cert operations, deployment timeline, real-time updates (SSE/WebSocket), target config wizard
- V2.1: Team Adoption — OIDC/SSO, RBAC, CLI tool, Slack/Teams notifiers, bulk cert import
- V2.2: Observability — expiration calendar, health scores, Prometheus metrics, deployment rollback
V3: Discovery & Visibility
Certificate discovery (passive/active scanning), unknown cert detection, triage workflows in GUI
V4+: Platform & Scale
Kubernetes CRD, Terraform provider, multi-region, HA control plane, 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.
On March 14, 2033 (7 years from initial release), the license automatically converts to Apache License 2.0 and all restrictions are removed.
For commercial licensing inquiries, contact skreddy040@gmail.com.