Closes Phase 9 of cowork/auth-bundle-2-prompt.md. Every Phase-5 HTTP
endpoint now has a matching MCP tool so operators driving certctl
from Claude / VS Code / any MCP client get the same OIDC-provider +
group-mapping + session management capability the GUI + CLI already
expose.
Coverage map (each tool → HTTP endpoint → permission)
=====================================================
certctl_auth_list_oidc_providers GET /v1/auth/oidc/providers auth.oidc.list
certctl_auth_get_oidc_provider GET /v1/auth/oidc/providers (filtered) auth.oidc.list
certctl_auth_create_oidc_provider POST /v1/auth/oidc/providers auth.oidc.create
certctl_auth_update_oidc_provider PUT /v1/auth/oidc/providers/{id} auth.oidc.edit
certctl_auth_delete_oidc_provider DELETE /v1/auth/oidc/providers/{id} auth.oidc.delete
certctl_auth_refresh_oidc_provider POST /v1/auth/oidc/providers/{id}/refresh auth.oidc.edit
certctl_auth_list_group_mappings GET /v1/auth/oidc/group-mappings?provider_id auth.oidc.list
certctl_auth_add_group_mapping POST /v1/auth/oidc/group-mappings auth.oidc.edit
certctl_auth_remove_group_mapping DELETE /v1/auth/oidc/group-mappings/{id} auth.oidc.edit
certctl_auth_list_sessions GET /v1/auth/sessions[?actor_id=&actor_type=] auth.session.list (own) | auth.session.list.all (other)
certctl_auth_revoke_session DELETE /v1/auth/sessions/{id} auth.session.revoke (or own-bypass)
Implementation notes
====================
internal/mcp/tools_auth_bundle2.go (NEW): 11 tools wired through three
focused register functions (registerAuthOIDCProviderTools,
registerAuthGroupMappingTools, registerAuthSessionTools). Every tool
routes through the existing Client (Get/Post/Put/Delete) so permission
gates fire server-side via the Phase-5 rbacGate wrappers — a non-admin
caller's MCP tool invocation gets whatever 403 the underlying HTTP
handler emits, not an MCP-side bypass.
Empty-id guard
--------------
Every path-id tool short-circuits to errorResult(fmt.Errorf("id is required"))
BEFORE the HTTP call. Defense against url.PathEscape("") collapsing a
singular op into the list endpoint (which would silently succeed against
a permissive backend). Same pattern across all 6 path-id tools (get,
update, delete, refresh provider; remove mapping; revoke session).
auth_get_oidc_provider list-then-filter
---------------------------------------
The Phase-5 HTTP API doesn't expose a singular GET /v1/auth/oidc/providers/{id}
endpoint — the GUI's OIDCProviderDetailPage fetches the full list and
filters in-process. The MCP tool mirrors that pattern exactly: GET the
list, JSON-decode the providers envelope, walk the array filtering by
id, return the matching raw JSON object on hit or an explicit "oidc
provider not found: <id>" error on miss. This keeps the MCP surface
in lockstep with the GUI's permission boundary (auth.oidc.list grants
"see any provider", as it does on the GUI) without inventing a new HTTP
endpoint.
internal/mcp/types.go (MODIFIED): 8 new input types matching the
Phase-5 wire shapes (oidcProviderRequest at internal/api/handler/auth_session_oidc.go).
client_secret on Update is optional — empty preserves the existing
ciphertext on the server, providing a value rotates. Mirrors the GUI's
edit-without-rotate UX from web/src/pages/auth/OIDCProviderDetailPage.tsx.
internal/mcp/tools.go (MODIFIED): registerAuthBundle2Tools wired into
RegisterTools alongside the Bundle 1 Phase 11 registerAuthTools.
Test coverage
=============
internal/mcp/tools_auth_bundle2_test.go (NEW), 5 test cases:
* TestAuthBundle2MCP_AllToolsRegister — registerAuthBundle2Tools
doesn't panic; catches duplicate-name regressions before CI.
* TestAuthBundle2MCP_PathsAndMethods — 11 cases (one per tool) +
the admin-other-actor variant of list_sessions; asserts the right
method + path + body + query string fires against the mock API.
* TestAuthBundle2MCP_ForbiddenSurfacesError — every tool's underlying
HTTP path returns a propagated error containing "forbidden" / "403"
when the mock returns 403, exercising the errorResult fence path.
* TestAuthBundle2MCP_GetProviderFiltersListByID — pins the list-then-
filter shape end-to-end with both the hit-and-return (returns the
matching raw JSON object) and miss-returns-error (sentinel string
"oidc provider not found") branches.
* TestAuthBundle2MCP_EmptyIDInputShortCircuits — pins the
strings.TrimSpace empty-id guard at the top of every path-id handler.
* TestAuthBundle2MCP_PromptCoverage — every tool the prompt enumerates
is also present in tools_per_tool_test.go's allHappyPathCases (so
the live-dispatch + 5xx error-path tests cover all 11 tools).
internal/mcp/tools_per_tool_test.go (MODIFIED): 11 new toolCase entries
in allHappyPathCases (live in-memory MCP dispatch + happy-path fence
shape + 5xx error-path fence shape) + a mock-API special case for
GET /api/v1/auth/oidc/providers that returns the right envelope shape
({"providers":[{"id":"op-okta",...}]}) so the get_oidc_provider tool's
in-process filter resolves under the live dispatch.
Verification
============
* gofmt + go vet — clean across internal/mcp/...
* go test -short -count=1 — green across internal/mcp + internal/auth/...
+ internal/api/handler + internal/api/router (13 packages, 0 failures).
* MCP tool count re-derive (CLAUDE.md command):
grep -cE 'mcp\.AddTool\(' internal/mcp/tools*.go
→ tools.go=121, tools_auth.go=12, tools_auth_bundle2.go=11 (new),
tools_est.go=6 — total 150. Matches the live count
TestMCP_RegisterTools_DispatchableToolCount asserts.
* staticcheck deferred — sandbox /tmp at 99% disk, can't install the
binary; all SA*/ST* lints would have run via the staticcheck-CI step
on push. go vet caught the only real issue (an unused context import)
before commit.
Not in this commit (deferred)
=============================
* Break-glass admin MCP tools (4 endpoints from Phase 7.5). The Phase 9
prompt does NOT enumerate break-glass tools; its exit criteria is
"Every API endpoint from Phase 5 has an MCP tool". Phase 5 does not
include the break-glass surface (Phase 7.5 ships those endpoints with
surface-invisibility semantics: 404 when CERTCTL_BREAKGLASS_ENABLED=false,
which complicates LLM tool-discovery UX). If the operator wants
break-glass MCP parity, that's a follow-on bundle.
certctl — Self-Hosted Certificate Lifecycle Platform
certctl is a self-hosted platform that automates the entire TLS 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. Free, source-available under BSL 1.1, covers the same lifecycle that enterprise platforms charge $100K+/year for.
The CA/Browser Forum's Ballot SC-081v3 caps public TLS certificates at 200 days by March 2026, 100 days by 2027, and 47 days by 2029. At 47-day lifespans, a team managing 100 certificates is processing 7+ renewals per week, every week, forever. Manual workflows stop being a choice.
Status: Early-access. Production-quality core (Local CA, ACME, agent deployment, CRUD, audit, role-based authz with auditor split + day-0 bootstrap + four-eyes approval) with broader feature surface (intermediate CA hierarchy, ACME/SCEP/EST servers, network appliances) still maturing. Federated identity (OIDC/SAML/WebAuthn, server-side sessions, break-glass accounts, JIT elevation) is the next slice on the roadmap, not yet shipped. Lab and dev deployments encouraged; production deployments welcome with the understanding that customer-scale battle-testing is in progress. File GitHub issues for any rough edges.
Actively maintained, shipping weekly. Open an issue if something breaks. CI runs the full test suite with race detection, static analysis, and vulnerability scanning on every commit.
Ready to try it? Jump to the Quick Start. For the marketing site, see certctl.io.
Documentation
The full audience-organized index lives at docs/README.md. Top-level entry points:
| Audience | Start here |
|---|---|
| New to certctl | Concepts → Quickstart → Examples |
| Production operator | Architecture → Security posture → Disaster recovery runbook |
| PKI engineer | ACME server → SCEP server → EST server → CA hierarchy |
| Migrating from another tool | from certbot / from acme.sh / cert-manager coexistence |
| Contributor | Architecture → Testing strategy → CI pipeline |
For the connector reference (12 issuers, 15 targets, 6 notifiers) see docs/reference/connectors/index.md.
Screenshots
Why certctl
Certificate lifecycle tooling has historically split into two camps. Enterprise platforms charge six-figure annual licenses, take months to deploy, and bill professional-services hours at $250 to $400 per hour to write integration code that should ship with the product. Single-purpose tools handle one slice of the problem and leave the operator to glue the rest together. certctl fills the gap — full lifecycle automation, self-hosted, free, CA-agnostic, target-agnostic. If you're stitching together cron jobs across a fleet, manually renewing certs, or writing custom integration scripts to bridge a commercial CLM platform to your actual infrastructure, certctl replaces all of that.
Built for platform engineering and DevOps teams managing 10 to 500+ certificates, security teams who need audit trails and policy enforcement, and small teams without enterprise budgets who need enterprise-grade automation for a 50-server environment. For the detailed positioning argument and when not to use certctl, see Why certctl?.
What it does
certctl handles the full certificate lifecycle in one self-hosted control plane:
- Issue and renew from any CA. Let's Encrypt and any ACME provider, an embedded ACME server you can point cert-manager / certbot / lego at directly, a built-in local CA with sub-CA mode (chains under your enterprise root like ADCS), step-ca, Vault PKI, EJBCA, AWS ACM PCA, Google CAS, DigiCert, Sectigo, GlobalSign, Entrust, plus an OpenSSL / shell-script adapter for anything custom. Twelve native issuer connectors. See the connector reference.
- Deploy automatically to NGINX, Apache, HAProxy, Caddy, Traefik, Envoy, IIS, Windows Cert Store, Java keystore, Kubernetes Secrets, AWS ACM, Azure Key Vault, SSH known-hosts, Postfix + Dovecot, F5 BIG-IP. Fifteen native target connectors. Every deploy goes through atomic-write + ownership-preservation + SHA-256 idempotency + per-target Prometheus counters + pre-deploy snapshot + on-failure rollback. See
docs/reference/deployment-model.md. - Run as an ACME server so existing client tooling plugs in directly. RFC 8555 + RFC 9773 ARI, two per-profile auth modes (public-trust-style validation or trust_authenticated for internal PKI), doubly-signed key rollover, revoke-cert on both kid path and jwk path, per-account rate limiting. Cert-manager / certbot / lego all work pointed at it. See
docs/reference/protocols/acme-server.md. - Run as a SCEP server for Microsoft Intune-managed phones, ChromeOS devices, network appliances. RFC 8894 native with full PKIMessage wire format, native Intune challenge dispatch with replay protection, per-profile dispatch with separate RA cert per profile. See
docs/reference/protocols/scep-server.md. - Run as an EST server for HTTPS-based PKCS#10 enrollment. 802.1X / Wi-Fi authentication, IoT device enrollment, RFC 9266 channel binding. See
docs/reference/protocols/est.md. - Manage multi-level CA hierarchies with name constraints, path-length enforcement, and end-to-end RFC 5280 path validation. Root → intermediate → issuing chains, admin-gated CRUD, drain-first retirement. Patterns documented for 4-level boundary CAs, 3-level policy CAs with per-BU
PermittedDNSDomains, and 2-level internal PKI. Seedocs/reference/intermediate-ca-hierarchy.md. - Gate high-stakes issuance behind two-person-integrity approval. Flag a profile as
RequiresApproval, the request lands in a queue, a non-requester approves, the scheduler dispatches. Profile-edit changes on approval-tier profiles route through the same gate so the flip-flop bypass is closed. Seedocs/operator/approval-workflow.md. - Authorize with role-based access control. Seven default roles (admin, operator, viewer, agent, mcp, cli, auditor) over a 33-permission canonical catalogue with global / per-profile / per-issuer scope. Auditor role is read-only on the audit trail (
audit.read+audit.export, nothing else) so a regulator's key cannot read certificates or mutate config. Day-0 admin via a one-shotCERTCTL_BOOTSTRAP_TOKENendpoint that closes itself the moment any admin lands. Privilege-escalation guard requiresauth.role.assignto grant or revoke a role. Seedocs/operator/rbac.md,docs/operator/auth-threat-model.md, and the v2.0.x → v2.1.0 migration guide. - Discover existing certs across your fleet via filesystem scanning on agents, network TLS probing across CIDR ranges, and cloud secret manager imports (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager). Triage workflow for claim / dismiss / investigate.
- Revoke with full RFC 5280 reason codes, DER CRL generation per issuer (scheduler-pre-generated and ETag-cached), and an embedded RFC 6960 OCSP responder with dedicated per-issuer responder certs. Single + bulk revocation. See
docs/reference/protocols/crl-ocsp.md. - Alert via Slack, Microsoft Teams, PagerDuty, OpsGenie, email, webhooks. Per-policy multi-channel routing matrix with severity tiers and fault-isolating per-channel dispatch. See
docs/operator/runbooks/expiry-alerts.md. - Drive the platform from natural language via the bundled MCP (Model Context Protocol) server. The full REST API is exposed as MCP tools — ask your AI client "show me all expiring certificates", "revoke the VPN cert, key compromised", or "what agents are offline?" and it translates to API calls. Stateless stdio-transport binary at
cmd/mcp-server/; same auth as the REST API; no extra attack surface. Seedocs/reference/mcp.md.
Architecture and security
Go 1.25 control plane with handler → service → repository layering. PostgreSQL 16 backend (35+ tables, idempotent migrations). Pull-only deployment model — the server never initiates outbound connections. Agents poll for work and generate ECDSA P-256 keys locally so private keys never touch the control plane. For network appliances and agentless servers, a proxy agent in the same network zone handles deployment via the target's API (WinRM, iControl REST, SSH/SFTP). See the Architecture Guide for full system diagrams.
Security: API-key authentication with SHA-256 hashing + constant-time comparison, then role-based authorization on every gated handler with global / per-profile / per-issuer scope. Auditor split keeps regulator-class actors strictly read-only on the audit trail. Day-0 admin via a one-shot bootstrap token; granting or revoking roles requires the dedicated auth.role.assign permission. CORS deny-by-default. Shell injection prevention on all connector scripts. SSRF protection (reserved IP filtering) on the network scanner. Issuer and target credentials encrypted at rest with AES-256-GCM. HTTPS-only control plane with TLS 1.3 pinned and a fail-closed startup gate that refuses to boot if the TLS bundle is unusable. Every API call recorded to an immutable audit trail with actor attribution, body hash, and latency tracking. CI runs race detection, 11 linters, and vulnerability scanning on every commit. See docs/operator/security.md for the full posture and docs/operator/auth-threat-model.md for what's defended vs deferred.
Quick Start
Docker Compose (recommended)
git clone https://github.com/certctl-io/certctl.git
cd certctl
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.demo.yml up -d --build
Wait ~30 seconds, then open https://localhost:8443 in your browser. The shipped demo overlay seeds 32 certificates across 10 issuers, 8 agents, and 180 days of realistic history. The certctl-tls-init init container self-signs an ECDSA-P256 cert on first boot — accept the browser warning for the demo, or feed the generated ca.crt to your client.
For a clean install without demo data, drop the -f deploy/docker-compose.demo.yml flag and run docker compose -f deploy/docker-compose.yml up -d --build. The four compose files (docker-compose.yml base, docker-compose.demo.yml overlay, docker-compose.dev.yml for PgAdmin + debug logging, docker-compose.test.yml for integration tests) are documented at deploy/ENVIRONMENTS.md.
curl --cacert $(docker compose -f deploy/docker-compose.yml exec -T certctl-server cat /etc/certctl/tls/ca.crt) https://localhost:8443/health
# {"status":"healthy"}
The control plane is HTTPS-only with TLS 1.3 pinned. See docs/operator/tls.md for cert provisioning patterns.
Agent install (one-liner)
curl -sSL https://raw.githubusercontent.com/certctl-io/certctl/master/install-agent.sh | bash
Detects your OS and architecture, downloads the binary, configures systemd (Linux) or launchd (macOS), and starts the agent. See install-agent.sh.
Helm chart (Kubernetes)
helm install certctl deploy/helm/certctl/ \
--set server.apiKey=your-api-key \
--set postgres.password=your-db-password
Production-ready chart with Server Deployment, PostgreSQL StatefulSet, Agent DaemonSet, health probes, security contexts (non-root, read-only rootfs), and optional Ingress. See values.yaml.
Container images
docker pull ghcr.io/certctl-io/certctl-server:latest
docker pull ghcr.io/certctl-io/certctl-agent:latest
Examples
Pick the scenario closest to your setup and have it running in 2 minutes:
| Example | Scenario |
|---|---|
examples/acme-nginx/ |
Let's Encrypt + NGINX, HTTP-01 challenges |
examples/acme-wildcard-dns01/ |
Wildcard certs via DNS-01 (Cloudflare hook included) |
examples/private-ca-traefik/ |
Local CA (self-signed or sub-CA) + Traefik file provider |
examples/step-ca-haproxy/ |
Smallstep step-ca + HAProxy combined PEM |
examples/multi-issuer/ |
ACME for public + Local CA for internal, one dashboard |
Each directory contains a docker-compose.yml and a README.md explaining the scenario, prerequisites, and customization.
Verifying a release
Every v* tag publishes signed, attested artefacts (Cosign keyless OIDC + SLSA Level 3 provenance + SPDX-JSON SBOMs). For the verification procedure, see docs/reference/release-verification.md.
Development
make build # Build server + agent binaries
make test # Run tests
make lint # golangci-lint (11 linters)
govulncheck ./... # Vulnerability scan
make docker-up # Start Docker Compose stack
CI runs go vet, go test -race, golangci-lint, govulncheck, and per-layer coverage thresholds (service 55%, handler 60%, domain 40%, middleware 30%) on every push. Frontend CI runs TypeScript type checking, Vitest tests, and Vite production build.
For the full contributor guide see docs/contributor/ — testing strategy, test environment, CI pipeline, QA prerequisites.
License
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 use certctl's certificate management functionality as part of a commercial certificate-management offering to third parties. See the LICENSE file for the full Additional Use Grant.
For licensing inquiries: certctl@proton.me
Dependencies
go list -m all | wc -l # total module count (direct + transitive)
go mod why <path> # explain why a module is pulled in
govulncheck ./... # vulnerability scan (CI runs this on every commit)
The release-time SBOM is published as an SPDX-JSON file alongside each release artifact.
If certctl solves a problem you have, star the repo to help others find it. Questions, bugs, or feature requests: open an issue.




