mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 15:18:53 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dedf7fa3a9 | |||
| 4b5927dfff | |||
| cc03f55006 | |||
| 93e1dc598c |
@@ -38,6 +38,8 @@ gantt
|
||||
|
||||
> **Actively maintained — shipping weekly.** Found something? [Open a GitHub issue](https://github.com/shankar0123/certctl/issues) — issues get triaged same-day. 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](#quick-start) — you'll have a running dashboard in under 5 minutes.
|
||||
|
||||
## 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, IIS on Windows, 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.
|
||||
@@ -46,7 +48,7 @@ certctl fills that gap. It's **CA-agnostic** — plug in any certificate authori
|
||||
|
||||
It's **target-agnostic**. Agents deploy certificates to NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, Postfix, Dovecot, IIS (local PowerShell or remote WinRM), F5 BIG-IP (proxy agent), and any Linux/Unix server via SSH/SFTP — all using the same pluggable connector model. 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, see [Why certctl?](docs/why-certctl.md)
|
||||
For a detailed comparison with other competitors and enterprise platforms, see [Why certctl?](docs/why-certctl.md)
|
||||
|
||||
## Who Is This For
|
||||
|
||||
@@ -60,7 +62,7 @@ For a detailed comparison with CertKit, KeyTalk, and enterprise platforms, see [
|
||||
|
||||
- **Certificates renew and deploy themselves.** The scheduler monitors expiration, creates renewal jobs, issues certificates through your CA, and deploys them to target servers — all without human intervention. ACME ARI (RFC 9773) lets your CA tell certctl exactly when to renew. Ready for 45-day and 6-day certificate lifetimes (SC-081v3 and Let's Encrypt shortlived profiles).
|
||||
|
||||
- **You see everything in one place.** A 25-page operational dashboard shows every certificate across every server: status, ownership, expiration timeline, deployment history with TLS verification, discovery triage, and real-time agent fleet health. Bulk operations (renew, revoke, reassign) work across selections.
|
||||
- **You see everything in one place.** The operational dashboard shows every certificate across every server: status, ownership, expiration timeline, deployment history with TLS verification, discovery triage, and real-time agent fleet health. Bulk operations (renew, revoke, reassign) work across selections.
|
||||
|
||||
- **Private keys never leave your servers.** Agents generate ECDSA P-256 keys locally and submit only the CSR. The control plane never touches private keys. Post-deployment TLS verification confirms the right certificate is actually being served.
|
||||
|
||||
@@ -68,7 +70,7 @@ For a detailed comparison with CertKit, KeyTalk, and enterprise platforms, see [
|
||||
|
||||
- **Everything is auditable.** Immutable append-only audit trail records every lifecycle action, every API call, and every approval decision. Certificate digest emails deliver daily briefings. Prometheus metrics endpoint for Grafana dashboards.
|
||||
|
||||
- **Multiple interfaces for different workflows.** REST API (97 endpoints) for automation, CLI for scripting, MCP server for AI assistants (Claude, Cursor, Windsurf), EST server (RFC 7030) for device enrollment, Helm chart for Kubernetes, and the web dashboard for day-to-day operations.
|
||||
- **Multiple interfaces for different workflows.** REST API for automation, CLI for scripting, MCP server for AI assistants (Claude, Cursor, Windsurf), EST server (RFC 7030) for device enrollment, Helm chart for Kubernetes, and the web dashboard for day-to-day operations.
|
||||
|
||||
For the full capability breakdown — revocation infrastructure (CRL + OCSP), policy engine, certificate profiles, S/MIME support, approval workflows, and more — see the [Feature Inventory](docs/features.md).
|
||||
|
||||
@@ -158,16 +160,19 @@ cd certctl
|
||||
docker compose -f deploy/docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
Wait ~30 seconds, then open **http://localhost:8443** in your browser.
|
||||
Wait ~30 seconds, then open **http://localhost:8443** in your browser. The onboarding wizard walks you through connecting a CA, deploying an agent, and issuing your first certificate.
|
||||
|
||||
The dashboard comes pre-loaded with 32 demo certificates across 7 issuers, 8 agents, 180 days of job history, discovery scan data, and network scan targets — a realistic snapshot of a certificate inventory that looks like it's been running for months.
|
||||
**Want a pre-populated demo instead?** Add the demo override to see 32 certificates across 7 issuers, 8 agents, and 180 days of realistic history:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.demo.yml up -d --build
|
||||
```
|
||||
|
||||
The `deploy/` directory has four compose files: `docker-compose.yml` (base platform), `docker-compose.demo.yml` (demo data overlay), `docker-compose.dev.yml` (PgAdmin + debug logging), and `docker-compose.test.yml` (standalone integration tests with real CA backends). See the [Quick Start Guide](docs/quickstart.md#docker-compose-environments) for details.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8443/health
|
||||
# {"status":"healthy"}
|
||||
|
||||
curl -s http://localhost:8443/api/v1/certificates | jq '.total'
|
||||
# 32
|
||||
```
|
||||
|
||||
### Agent Install (One-Liner)
|
||||
@@ -221,9 +226,15 @@ Each directory contains a `docker-compose.yml` and a `README.md` explaining the
|
||||
| [Advanced Demo](docs/demo-advanced.md) | Issue a certificate end-to-end with technical deep-dives |
|
||||
| [Architecture](docs/architecture.md) | System design, data flow diagrams, security model |
|
||||
| [Feature Inventory](docs/features.md) | Complete reference of all V2 capabilities, API endpoints, and configuration |
|
||||
| [Connector Reference](docs/connectors.md) | Configuration for all 7 issuers, 10 targets, and 5 notifier connectors |
|
||||
| [Connector Reference](docs/connectors.md) | Configuration for all issuer, target, and notifier connectors |
|
||||
| [MCP Server](docs/mcp.md) | AI integration via Model Context Protocol — setup, available tools, examples |
|
||||
| [OpenAPI 3.1 Spec](docs/openapi.md) | API reference guide with endpoint overview ([raw spec](api/openapi.yaml)) |
|
||||
| [Compliance Mapping](docs/compliance.md) | SOC 2 Type II, PCI-DSS 4.0, NIST SP 800-57 alignment guides |
|
||||
| [OpenAPI 3.1 Spec](api/openapi.yaml) | 97 operations, full request/response schemas |
|
||||
| [Migrate from certbot](docs/migrate-from-certbot.md) | Step-by-step migration from certbot cron jobs to certctl |
|
||||
| [Migrate from acme.sh](docs/migrate-from-acmesh.md) | Migration guide for acme.sh users, DNS hook compatibility |
|
||||
| [certctl for cert-manager users](docs/certctl-for-cert-manager-users.md) | How certctl complements cert-manager for mixed infrastructure |
|
||||
| [Test Environment](docs/test-env.md) | Docker Compose test environment with real CA backends |
|
||||
| [Testing Guide](docs/testing-guide.md) | Comprehensive test procedures, smoke tests, and release sign-off checklist |
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -303,7 +314,7 @@ Core lifecycle management — Local CA + ACME v2 issuers, NGINX target connector
|
||||
Team access controls and identity provider integration (OIDC/SSO). Role-based access control with profile-gating. Event-driven architecture (NATS) with real-time operational views. Advanced search DSL, compliance and risk scoring, bulk fleet operations.
|
||||
|
||||
### 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 (Google CAS, EJBCA, Sectigo), and platform-scale features (Terraform provider, multi-tenancy, HSM support).
|
||||
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 (Entrust, GlobalSign, EJBCA), and platform-scale features (Terraform provider, multi-tenancy, HSM support).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ The server exposes a REST API under `/api/v1/` and optionally serves the web das
|
||||
|
||||
### Agents
|
||||
|
||||
Lightweight Go processes that run on or near your infrastructure. Agents generate ECDSA P-256 private keys locally, create CSRs, and submit them to the control plane for signing — private keys never leave agent infrastructure. Agents also handle certificate deployment to target systems (NGINX, Apache httpd, HAProxy, Traefik, Caddy, Envoy, Postfix, Dovecot, IIS fully implemented; F5 BIG-IP interface stub only) and report job status. They communicate with the control plane via HTTP and authenticate with API keys.
|
||||
Lightweight Go processes that run on or near your infrastructure. Agents generate ECDSA P-256 private keys locally, create CSRs, and submit them to the control plane for signing — private keys never leave agent infrastructure. Agents also handle certificate deployment to target systems (NGINX, Apache httpd, HAProxy, Traefik, Caddy, Envoy, Postfix, Dovecot, IIS, F5 BIG-IP, SSH, Windows Certificate Store, Java Keystore) and report job status. They communicate with the control plane via HTTP and authenticate with API keys.
|
||||
|
||||
The agent runs two background loops: a heartbeat (every 60 seconds) to signal it's alive, and a work poll (every 30 seconds) to check for actionable jobs via `GET /api/v1/agents/{id}/work`. Jobs may be `AwaitingCSR` (agent needs to generate key + submit CSR) or `Deployment` (agent needs to deploy a certificate). Private keys are stored in `CERTCTL_KEY_DIR` (default `/var/lib/certctl/keys`) with 0600 permissions.
|
||||
|
||||
@@ -602,7 +602,7 @@ type Connector interface {
|
||||
|
||||
The `DeploymentRequest` struct carries the full material needed by the target system: the signed certificate, the CA chain, the agent-generated private key, target-specific configuration, and arbitrary metadata. The key field is populated by the agent from its local key store (`CERTCTL_KEY_DIR`) — it never originates from the control plane.
|
||||
|
||||
Built-in targets: **NGINX** (writes cert/chain/key files, validates with `nginx -t`, reloads), **Apache httpd** (writes cert/chain/key files, validates with `apachectl configtest`, graceful reload), **HAProxy** (combined PEM file with cert+chain+key, validates config, reloads via systemctl/signal), **Traefik** (file provider — writes cert/key to watched directory, Traefik auto-reloads), **Caddy** (dual-mode: admin API hot-reload or file-based), **F5 BIG-IP** (interface only — proxy agent + iControl REST, implementation planned), **IIS** (interface only — dual-mode: agent-local PowerShell primary + proxy agent WinRM for agentless targets, implementation planned).
|
||||
Built-in targets: **NGINX** (writes cert/chain/key files, validates with `nginx -t`, reloads), **Apache httpd** (writes cert/chain/key files, validates with `apachectl configtest`, graceful reload), **HAProxy** (combined PEM file with cert+chain+key, validates config, reloads via systemctl/signal), **Traefik** (file provider — writes cert/key to watched directory, Traefik auto-reloads), **Caddy** (dual-mode: admin API hot-reload or file-based), **Envoy** (file-based with optional SDS JSON config), **F5 BIG-IP** (proxy agent + iControl REST, transaction-based atomic SSL profile updates), **IIS** (dual-mode: agent-local PowerShell + proxy agent WinRM for agentless targets), **Postfix/Dovecot** (file write + service reload), **SSH** (agentless deployment via SSH/SFTP), **Windows Certificate Store** (PowerShell-based cert import, dual-mode local/WinRM), **Java Keystore** (PEM → PKCS#12 → keytool pipeline, JKS and PKCS12 formats).
|
||||
|
||||
After deployment, agents can perform **post-deployment TLS verification**: the agent probes the live TLS endpoint using `crypto/tls.DialWithDialer` and compares the SHA-256 fingerprint of the served certificate against what was deployed. Results are reported via `POST /api/v1/jobs/{id}/verify` and stored on the job record. Verification is best-effort — failures don't block or rollback deployments.
|
||||
|
||||
@@ -810,7 +810,7 @@ flowchart LR
|
||||
AI["AI Assistant\n(Claude, Cursor)"] -->|"stdio"| MCP["MCP Server\ncmd/mcp-server/"]
|
||||
MCP -->|"HTTP + Bearer token"| API["certctl REST API\n:8443"]
|
||||
|
||||
subgraph "78 MCP Tools"
|
||||
subgraph "MCP Tools"
|
||||
T1["Certificate CRUD"]
|
||||
T2["Agent Management"]
|
||||
T3["Job Operations"]
|
||||
@@ -824,7 +824,7 @@ flowchart LR
|
||||
|
||||
The MCP server is a stateless HTTP proxy — every MCP tool call translates to an HTTP request to the certctl REST API. It adds no new state, no new dependencies, and no new attack surface beyond what the API already exposes. Configuration is minimal: `CERTCTL_SERVER_URL` and `CERTCTL_API_KEY` environment variables.
|
||||
|
||||
The 78 tools are organized across 16 resource domains with typed input structs and `jsonschema` struct tags for automatic LLM-friendly schema generation. Binary response support handles DER CRL and OCSP endpoints.
|
||||
The tools are organized across 16 resource domains with typed input structs and `jsonschema` struct tags for automatic LLM-friendly schema generation. Binary response support handles DER CRL and OCSP endpoints.
|
||||
|
||||
## CLI Tool
|
||||
|
||||
@@ -986,6 +986,8 @@ certctl is extensively tested across eight layers with CI-enforced coverage gate
|
||||
|
||||
**CI pipeline** (`.github/workflows/ci.yml`) — Two parallel jobs. Go: build, vet, `go test -race`, `golangci-lint` (11 linters), `govulncheck`, test with coverage, per-layer coverage threshold enforcement (service 60%, handler 60%, domain 40%, middleware 50%). Frontend: TypeScript type check, Vitest, Vite production build.
|
||||
|
||||
For detailed test procedures, smoke tests, and the release sign-off checklist, see the [Testing Guide](testing-guide.md). For setting up the Docker Compose test environment with real CA backends, see [Test Environment](test-env.md).
|
||||
|
||||
## What's Next
|
||||
|
||||
- [Quick Start](quickstart.md) — Get certctl running locally
|
||||
@@ -994,3 +996,5 @@ certctl is extensively tested across eight layers with CI-enforced coverage gate
|
||||
- [Compliance Mapping](compliance.md) — SOC 2, PCI-DSS 4.0, and NIST SP 800-57 alignment
|
||||
- [MCP Server Guide](mcp.md) — AI-native access to the API
|
||||
- [OpenAPI Spec](openapi.md) — Full API reference and SDK generation
|
||||
- [Testing Guide](testing-guide.md) — Test procedures and release sign-off
|
||||
- [Test Environment](test-env.md) — Docker Compose test environment setup
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ The CLI supports both table and JSON output formats (`--format table` or `--form
|
||||
|
||||
### MCP Server (AI Integration)
|
||||
|
||||
certctl includes an MCP (Model Context Protocol) server that exposes 78 MCP tools covering the REST API. This enables AI assistants like Claude, Cursor, and other MCP-compatible tools to interact with your certificate infrastructure using natural language — "show me all expiring certificates," "revoke the VPN cert," or "what agents are offline?"
|
||||
certctl includes an MCP (Model Context Protocol) server that exposes the entire REST API as MCP tools. This enables AI assistants like Claude, Cursor, and other MCP-compatible tools to interact with your certificate infrastructure using natural language — "show me all expiring certificates," "revoke the VPN cert," or "what agents are offline?"
|
||||
|
||||
The MCP server is a separate binary (`cmd/mcp-server/`) that communicates via stdio transport and acts as a stateless HTTP proxy to the certctl REST API. It requires no additional infrastructure — just point it at your certctl server URL and API key.
|
||||
|
||||
|
||||
+1
-2
@@ -13,7 +13,6 @@ Connectors extend certctl to integrate with external systems for certificate iss
|
||||
- [OpenSSL / Custom CA](#openssl--custom-ca)
|
||||
- [Revocation Across Issuers](#revocation-across-issuers)
|
||||
- [EST Integration (GetCACertPEM)](#est-integration-getcacertpem)
|
||||
- [Planned Issuers](#planned-issuers)
|
||||
- [Building a Custom Issuer](#building-a-custom-issuer)
|
||||
3. [Target Connector](#target-connector)
|
||||
- [Interface](#interface-1)
|
||||
@@ -24,7 +23,7 @@ Connectors extend certctl to integrate with external systems for certificate iss
|
||||
- [Built-in: Envoy](#built-in-envoy)
|
||||
- [Built-in: Postfix / Dovecot](#built-in-postfix--dovecot)
|
||||
- [Built-in: Caddy](#built-in-caddy)
|
||||
- [F5 BIG-IP (Interface Only)](#f5-big-ip-interface-only)
|
||||
- [F5 BIG-IP (Implemented)](#f5-big-ip-implemented)
|
||||
- [IIS (Implemented, Dual-Mode)](#iis-implemented-dual-mode)
|
||||
- [SSH (Agentless Deployment)](#ssh-agentless-deployment)
|
||||
- [Windows Certificate Store](#windows-certificate-store)
|
||||
|
||||
@@ -981,7 +981,7 @@ export CERTCTL_API_KEY="test-key-123"
|
||||
|
||||
## Part 15: MCP Server for AI Integration (M18a)
|
||||
|
||||
certctl exposes 78 MCP tools covering the REST API via the Model Context Protocol (MCP), enabling seamless integration with Claude, Cursor, and other AI assistants:
|
||||
certctl exposes the full REST API via the Model Context Protocol (MCP), enabling seamless integration with Claude, Cursor, and other AI assistants:
|
||||
|
||||
```bash
|
||||
# Build the MCP server
|
||||
|
||||
+26
-23
@@ -7,7 +7,7 @@ Complete reference of all features shipped in the V2 release (as of March 2026).
|
||||
## API Surface
|
||||
|
||||
### Overview
|
||||
- **99 endpoints** across 23 resource domains under `/api/v1/` + `/.well-known/est/`
|
||||
- REST API across 23 resource domains under `/api/v1/` + `/.well-known/est/`
|
||||
- REST API with HTTP semantics (GET, POST, PUT, DELETE)
|
||||
- All endpoints require authentication by default (configurable)
|
||||
- OpenAPI 3.1 spec with full schema documentation
|
||||
@@ -1134,12 +1134,12 @@ The web dashboard is the primary operational interface for certctl. Built with *
|
||||
## Integration Interfaces
|
||||
|
||||
### MCP Server (M18a)
|
||||
**Separate binary** (`cmd/mcp-server/`) providing AI-native access to certctl via Claude, Cursor, OpenClaw. Instead of memorizing 91 API endpoints, ask your AI assistant "what certificates are expiring this week?" or "renew the API prod cert" and it translates to the right API calls.
|
||||
**Separate binary** (`cmd/mcp-server/`) providing AI-native access to certctl via Claude, Cursor, OpenClaw. Instead of memorizing API endpoints, ask your AI assistant "what certificates are expiring this week?" or "renew the API prod cert" and it translates to the right API calls.
|
||||
|
||||
- **Transport** — stdio (stdin/stdout)
|
||||
- **Protocol** — Model Context Protocol v1
|
||||
- **SDK** — Official `modelcontextprotocol/go-sdk` v1.4.1
|
||||
- **Tools** — 78 MCP tools covering all API endpoints
|
||||
- **Tools** — MCP tools covering all API endpoints
|
||||
- **Organization** — 16 resource domains (Certificates, Issuers, Targets, Agents, Jobs, etc.)
|
||||
- **Authentication** — Bearer token via `CERTCTL_API_KEY` env var
|
||||
- **Configuration** — `CERTCTL_SERVER_URL` (e.g., http://localhost:8080) + `CERTCTL_API_KEY`
|
||||
@@ -1439,8 +1439,8 @@ Each guide includes an evidence summary table mapping specific criteria to certc
|
||||
| Feature | V2 | V3 (Paid) | Status |
|
||||
|---------|----|-----------|-|
|
||||
| Certificate lifecycle (create/renew/revoke) | ✓ | ✓ | Shipped v1.0+ |
|
||||
| 4 issuer connectors (Local CA, ACME, step-ca, OpenSSL) | ✓ | ✓ | Shipped |
|
||||
| 3 target connectors (NGINX, Apache, HAProxy) | ✓ | ✓ | Shipped |
|
||||
| 9 issuer connectors (Local CA, ACME, step-ca, OpenSSL, Vault PKI, DigiCert, Sectigo, Google CAS, EST) | ✓ | ✓ | Shipped |
|
||||
| 13 target connectors (NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, IIS, F5, Postfix, Dovecot, SSH, WinCertStore, JavaKeystore) | ✓ | ✓ | Shipped |
|
||||
| 6 notifier channels (Email, Webhook, Slack, Teams, PagerDuty, OpsGenie) | ✓ | ✓ | Shipped |
|
||||
| Agent fleet + metadata | ✓ | ✓ | Shipped |
|
||||
| Agent groups (dynamic + manual) | ✓ | ✓ | Shipped |
|
||||
@@ -1449,28 +1449,33 @@ Each guide includes an evidence summary table mapping specific criteria to certc
|
||||
| Revocation (RFC 5280, CRL, OCSP) | ✓ | ✓ | Shipped |
|
||||
| Full web dashboard | ✓ | ✓ | Shipped |
|
||||
| Observability (charts, metrics, stats) | ✓ | ✓ | Shipped |
|
||||
| REST API (91 endpoints) | ✓ | ✓ | Shipped |
|
||||
| MCP server (78 tools) | ✓ | ✓ | Shipped v2.1 |
|
||||
| CLI tool (12 subcommands) | ✓ | ✓ | Shipped |
|
||||
| REST API | ✓ | ✓ | Shipped |
|
||||
| MCP server (REST API exposed via MCP) | ✓ | ✓ | Shipped v2.1 |
|
||||
| CLI tool | ✓ | ✓ | Shipped |
|
||||
| Compliance mapping docs (SOC 2, PCI-DSS, NIST) | ✓ | ✓ | Shipped |
|
||||
| Filesystem cert discovery (M18b) | ✓ | ✓ | Shipped |
|
||||
| Network cert discovery (M21) | ✓ | ✓ | Shipped |
|
||||
| Prometheus metrics (M22) | ✓ | ✓ | Shipped |
|
||||
| Filesystem cert discovery | ✓ | ✓ | Shipped |
|
||||
| Network cert discovery | ✓ | ✓ | Shipped |
|
||||
| Prometheus metrics | ✓ | ✓ | Shipped |
|
||||
| Enhanced query API (sort, filter, cursor, fields) | ✓ | ✓ | Shipped |
|
||||
| Immutable API audit log | ✓ | ✓ | Shipped |
|
||||
| Bulk operations | ✓ | ✓ | Shipped |
|
||||
| EST server (RFC 7030) | ✓ | ✓ | Shipped |
|
||||
| Post-deployment TLS verification | ✓ | ✓ | Shipped |
|
||||
| Certificate export (PEM + PKCS#12) | ✓ | ✓ | Shipped |
|
||||
| S/MIME support (EKU-aware issuance) | ✓ | ✓ | Shipped |
|
||||
| ACME ARI (RFC 9773) | ✓ | ✓ | Shipped |
|
||||
| Scheduled certificate digest emails | ✓ | ✓ | Shipped |
|
||||
| Helm chart (Kubernetes) | ✓ | ✓ | Shipped |
|
||||
| Dynamic issuer/target configuration (GUI) | ✓ | ✓ | Shipped |
|
||||
| Onboarding wizard | ✓ | ✓ | Shipped |
|
||||
| **OIDC/SSO auth** | ✗ | ✓ | Planned V3 |
|
||||
| **RBAC (role-based access control)** | ✗ | ✓ | Planned V3 |
|
||||
| **F5 BIG-IP implementation** | Stub | ✓ | Planned V3 |
|
||||
| **IIS implementation** | Stub | ✓ | Planned V3 |
|
||||
| **NATS event bus** | ✗ | ✓ | Planned V3 |
|
||||
| **Real-time updates (SSE/WebSocket)** | ✗ | ✓ | Planned V3 |
|
||||
| **Advanced search DSL** | ✗ | ✓ | Planned V3 |
|
||||
| **Bulk operations** | ✓ | ✓ | M13 (free) |
|
||||
| **Bulk revocation** | ✗ | ✓ | Planned V3 (paid) |
|
||||
| **Bulk revocation (by profile/owner/agent)** | ✗ | ✓ | Planned V3 |
|
||||
| **Certificate health scores** | ✗ | ✓ | Planned V3 |
|
||||
| **Compliance scoring** | ✗ | ✓ | Planned V3 |
|
||||
| **DigiCert issuer** | ✗ | ✓ | Implemented (Beta) |
|
||||
| **Vault PKI issuer** | ✗ | ✓ | Implemented (Beta) |
|
||||
|
||||
---
|
||||
|
||||
@@ -1478,10 +1483,9 @@ Each guide includes an evidence summary table mapping specific criteria to certc
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| **API Endpoints** | 97 (under /api/v1/ + /.well-known/est/) |
|
||||
| **Dashboard** | Full web GUI |
|
||||
| **Issuer Connectors** | 6 (Local CA, ACME, step-ca, OpenSSL, Vault PKI, DigiCert) |
|
||||
| **Target Connectors** | 10 (9 impl: NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, IIS, Postfix, Dovecot; 1 stub: F5) |
|
||||
| **Dashboard** | Full web GUI with operational views wired to real API data |
|
||||
| **Issuer Connectors** | 8 (Local CA, ACME, step-ca, OpenSSL, Vault PKI, DigiCert, Sectigo, Google CAS) + EST server |
|
||||
| **Target Connectors** | 13 (NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, IIS, F5, Postfix, Dovecot, SSH, WinCertStore, JavaKeystore) |
|
||||
| **Notifier Channels** | 6 (Email, Webhook, Slack, Teams, PagerDuty, OpsGenie) |
|
||||
| **Job Types** | 4 (Issuance, Renewal, Deployment, Validation) |
|
||||
| **Job States** | 7 (Pending, AwaitingCSR, AwaitingApproval, Running, Completed, Failed, Cancelled) |
|
||||
@@ -1489,9 +1493,8 @@ Each guide includes an evidence summary table mapping specific criteria to certc
|
||||
| **Certificate States** | 8 (Pending, Active, Expiring, Expired, RenewalInProgress, Failed, Revoked, Archived) |
|
||||
| **Revocation Reason Codes** | 8 (RFC 5280 compliant) |
|
||||
| **Discovery Statuses** | 3 (Unmanaged, Managed, Dismissed) |
|
||||
| **MCP Tools** | 76 (16 resource domains) |
|
||||
| **MCP Server** | REST API exposed via MCP (16 resource domains) |
|
||||
| **CLI Subcommands** | 10 |
|
||||
| **Database Tables** | 19 |
|
||||
| **Test Suite** | Extensively tested with CI-enforced coverage gates |
|
||||
| **Environment Variables** | 41+ configuration options |
|
||||
|
||||
|
||||
+2
-2
@@ -94,7 +94,7 @@ Add certctl as an MCP server in your project's `.mcp.json`:
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server registers 78 tools organized across 16 resource domains:
|
||||
The MCP server exposes the full REST API organized across 16 resource domains:
|
||||
|
||||
| Domain | Tools | Examples |
|
||||
|--------|-------|---------|
|
||||
@@ -153,7 +153,7 @@ flowchart LR
|
||||
AI <-->|"stdio"| MCP
|
||||
MCP -->|"HTTP + Bearer token"| SERVER
|
||||
|
||||
MCP ~~~ TOOLS["78 tools · 16 domains\nTyped input structs"]
|
||||
MCP ~~~ TOOLS["REST API via MCP · 16 domains\nTyped input structs"]
|
||||
```
|
||||
|
||||
The MCP server is intentionally thin:
|
||||
|
||||
+14
-1
@@ -60,6 +60,19 @@ cp deploy/.env.example deploy/.env
|
||||
docker compose -f deploy/docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
### Docker Compose Environments
|
||||
|
||||
The `deploy/` directory contains four compose files for different use cases:
|
||||
|
||||
| File | Purpose | How to run |
|
||||
|------|---------|------------|
|
||||
| `docker-compose.yml` | **Base platform.** PostgreSQL + certctl server + agent. Clean dashboard with onboarding wizard — use this for production or first-time setup. | `docker compose -f deploy/docker-compose.yml up --build` |
|
||||
| `docker-compose.demo.yml` | **Demo data override.** Layers 180 days of realistic seed data (15 certs, 5 agents, multiple issuers) onto the base. Dashboard charts and tables look populated on first boot. | `docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.demo.yml up --build` |
|
||||
| `docker-compose.dev.yml` | **Development override.** Adds PgAdmin (port 5050), debug-level logging, and a Delve debugger port (40000) for the server. | `docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml up --build` |
|
||||
| `docker-compose.test.yml` | **Integration test environment.** 7 containers on a static-IP subnet: PostgreSQL, certctl server+agent, step-ca, Pebble ACME server, challenge test server, and NGINX. Runs the full issuance→deployment→verification flow against real CA backends. Standalone — does not combine with the base file. | `docker compose -f deploy/docker-compose.test.yml up --build` |
|
||||
|
||||
Override files are layered onto the base with multiple `-f` flags. The test environment is self-contained and runs independently. To reset any environment's data, add `down -v` to remove volumes.
|
||||
|
||||
### Kubernetes with Helm
|
||||
|
||||
For production deployments on Kubernetes, use the Helm chart:
|
||||
@@ -404,7 +417,7 @@ export CERTCTL_API_KEY="test-key-123"
|
||||
./mcp-server
|
||||
```
|
||||
|
||||
Exposes 78 MCP tools covering the REST API via stdio transport. Ask Claude: "What certificates are expiring in the next 30 days?", "Revoke the payments cert due to key compromise", "Show me the audit trail."
|
||||
Exposes the full REST API via MCP over stdio transport. Ask Claude: "What certificates are expiring in the next 30 days?", "Revoke the payments cert due to key compromise", "Show me the audit trail."
|
||||
|
||||
## Demo Data Reference
|
||||
|
||||
|
||||
@@ -3276,7 +3276,7 @@ timeout 3 bash -c 'CERTCTL_API_KEY=$API_KEY ./certctl-mcp 2>&1' || true
|
||||
|
||||
### 18.2 Tool Registration
|
||||
|
||||
**Test 18.2.1 — Tool count verification (78 tools)**
|
||||
**Test 18.2.1 — Tool count verification**
|
||||
|
||||
```bash
|
||||
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \
|
||||
@@ -6006,7 +6006,7 @@ These must be green before starting manual QA:
|
||||
| 18.1.1 | Binary builds successfully | Manual | ☐ | | |
|
||||
| 18.1.2 | Startup with valid env vars | Manual | ☐ | | |
|
||||
| 18.1.3 | Missing CERTCTL_SERVER_URL behavior | Manual | ☐ | | |
|
||||
| 18.2.1 | Tool count verification (78 tools) | Manual | ☐ | | |
|
||||
| 18.2.1 | Tool count verification | Manual | ☐ | | |
|
||||
| 18.2.2 | All 16 resource domains present | Manual | ☐ | | |
|
||||
| 18.3.1 | List certificates via MCP | Manual | ☐ | | |
|
||||
| 18.3.2 | Get specific certificate via MCP | Manual | ☐ | | |
|
||||
|
||||
+10
-8
@@ -32,11 +32,13 @@ This isn't a premium feature. It's the default behavior, free. Most alternatives
|
||||
|
||||
### 2. CA-Agnostic Issuer Architecture
|
||||
|
||||
certctl works with any certificate authority, not just ACME providers. Seven issuer connectors ship today, all free:
|
||||
certctl works with any certificate authority, not just ACME providers. Nine issuer connectors ship today, all free:
|
||||
|
||||
- **ACME v2** (Let's Encrypt, ZeroSSL, Google Trust Services, Buypass) — HTTP-01, DNS-01, DNS-PERSIST-01 challenges, External Account Binding, ACME Renewal Information (RFC 9773)
|
||||
- **ACME v2** (Let's Encrypt, ZeroSSL, Google Trust Services, Buypass) — HTTP-01, DNS-01, DNS-PERSIST-01 challenges, External Account Binding, ACME Renewal Information (RFC 9773), certificate profile selection
|
||||
- **HashiCorp Vault PKI** — `/v1/{mount}/sign/{role}` API, token auth
|
||||
- **DigiCert CertCentral** — async order model, OV/EV support
|
||||
- **Sectigo SCM** — async order model, DV/OV/EV support, 3-header auth
|
||||
- **Google Cloud CAS** — Certificate Authority Service, OAuth2 service account auth, CA pool selection
|
||||
- **step-ca** (Smallstep) — native /sign API with JWK provisioner auth
|
||||
- **Local CA** — self-signed or sub-CA mode (chain to ADCS or any enterprise root)
|
||||
- **OpenSSL / Custom CA** — delegate signing to any shell script
|
||||
@@ -54,7 +56,7 @@ A reload command can exit 0 while the certificate doesn't take effect — wrong
|
||||
|
||||
The three differentiators above get the headlines, but the feature surface is wider than most paid platforms:
|
||||
|
||||
**10 deployment targets** — NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, IIS (local PowerShell + remote WinRM), Postfix, and Dovecot. All use a pluggable connector model. The control plane never initiates outbound connections — agents poll for work, meaning certctl works behind firewalls, across network zones, and in air-gapped environments.
|
||||
**13 deployment targets** — NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, IIS (local PowerShell + remote WinRM), F5 BIG-IP (proxy agent + iControl REST), Postfix, Dovecot, SSH (agentless), Windows Certificate Store, and Java Keystore. All use a pluggable connector model. The control plane never initiates outbound connections — agents poll for work, meaning certctl works behind firewalls, across network zones, and in air-gapped environments.
|
||||
|
||||
**Network certificate discovery** — active TLS scanning of CIDR ranges finds certificates you didn't know existed. Agents also scan local filesystems for PEM/DER files. Everything feeds into a triage workflow where you claim, dismiss, or import discovered certs into management.
|
||||
|
||||
@@ -66,9 +68,9 @@ The three differentiators above get the headlines, but the feature surface is wi
|
||||
|
||||
**Prometheus metrics** — `/api/v1/metrics/prometheus` in standard exposition format. Works with Prometheus, Grafana Agent, Datadog Agent, Victoria Metrics.
|
||||
|
||||
**MCP server** — 80 tools exposing the entire API surface for AI-assisted certificate management via Claude, Cursor, or any MCP-compatible client. No other certificate platform offers this.
|
||||
**MCP server** — the entire REST API is exposed via MCP for AI-assisted certificate management via Claude, Cursor, or any MCP-compatible client. No other certificate platform offers this.
|
||||
|
||||
**Full REST API** — 97 OpenAPI 3.1-documented operations. CLI tool with 10 subcommands. Helm chart for Kubernetes deployment. Scheduled certificate digest emails. Certificate export in PEM and PKCS#12. S/MIME support with EKU-aware issuance.
|
||||
**Full REST API** — OpenAPI 3.1-documented operations covering the entire platform. CLI tool with 10 subcommands. Helm chart for Kubernetes deployment. Scheduled certificate digest emails. Certificate export in PEM and PKCS#12. S/MIME support with EKU-aware issuance.
|
||||
|
||||
**Extensively tested** — Go backend with race detection, static analysis (golangci-lint), and vulnerability scanning (govulncheck) on every commit. CI-enforced per-layer coverage thresholds. Frontend test suite. Every push is gated.
|
||||
|
||||
@@ -80,11 +82,11 @@ ACME clients solve one slice of the problem — issuance and renewal from ACME C
|
||||
|
||||
### vs. Agent-Based SaaS
|
||||
|
||||
The closest architectural competitors use the same agent model — local key generation, CSR submission, push-based deployment. Where certctl differs: it supports 7 issuer types (not just ACME), provides CRL/OCSP/revocation infrastructure (not just issuance), includes a policy engine and network discovery, and is source-available with no certificate limit. SaaS alternatives are typically proprietary, priced per certificate ($2+/cert/month), and cap their free tiers at 3-5 certificates. certctl is free for any number of certificates, forever.
|
||||
The closest architectural competitors use the same agent model — local key generation, CSR submission, push-based deployment. Where certctl differs: it supports 9 issuer types (not just ACME), provides CRL/OCSP/revocation infrastructure (not just issuance), includes a policy engine and network discovery, and is source-available with no certificate limit. SaaS alternatives are typically proprietary, priced per certificate ($2+/cert/month), and cap their free tiers at 3-5 certificates. certctl is free for any number of certificates, forever.
|
||||
|
||||
### vs. Commercial PKI Platforms
|
||||
|
||||
On-prem or hosted commercial platforms offer broader cert type coverage (VPN certs, device auth, SCEP) and deeper CA integrations. The trade-off: no free tier, opaque pricing (often €13K+/year for 1,500 certs), proprietary codebases, and no public API documentation. certctl trades breadth of exotic cert types for full transparency — source-available code, 97-operation OpenAPI spec, and a free community edition with no artificial limits.
|
||||
On-prem or hosted commercial platforms offer broader cert type coverage (VPN certs, device auth, SCEP) and deeper CA integrations. The trade-off: no free tier, opaque pricing (often €13K+/year for 1,500 certs), proprietary codebases, and no public API documentation. certctl trades breadth of exotic cert types for full transparency — source-available code, fully documented OpenAPI spec, and a free community edition with no artificial limits.
|
||||
|
||||
### vs. Enterprise Platforms
|
||||
|
||||
@@ -100,7 +102,7 @@ certctl isn't the right tool for everyone:
|
||||
|
||||
## See It Running
|
||||
|
||||
The demo seeds 32 certificates across 7 issuers, 8 agents, 6 deployment targets, and 180 days of realistic history — jobs, audit events, discovery scans, approval workflows — so you can explore every feature immediately.
|
||||
The demo seeds certificates across multiple issuers, agents, and deployment targets with 180 days of realistic history — jobs, audit events, discovery scans, approval workflows — so you can explore every feature immediately.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/shankar0123/certctl.git
|
||||
|
||||
@@ -536,6 +536,11 @@ func (s *IssuerService) CreateIssuer(iss domain.Issuer) (*domain.Issuer, error)
|
||||
if iss.Source == "" {
|
||||
iss.Source = "database"
|
||||
}
|
||||
// GUI-created issuers should be enabled by default.
|
||||
// Go's bool zero value is false, which overrides the DB default when explicitly inserted.
|
||||
if iss.Source == "database" && !iss.Enabled {
|
||||
iss.Enabled = true
|
||||
}
|
||||
|
||||
// Encrypt config
|
||||
if len(iss.Config) > 0 {
|
||||
|
||||
@@ -284,6 +284,11 @@ func (s *TargetService) CreateTarget(target domain.DeploymentTarget) (*domain.De
|
||||
if target.Source == "" {
|
||||
target.Source = "database"
|
||||
}
|
||||
// GUI-created targets should be enabled by default.
|
||||
// Go's bool zero value is false, which overrides the DB default when explicitly inserted.
|
||||
if target.Source == "database" && !target.Enabled {
|
||||
target.Enabled = true
|
||||
}
|
||||
|
||||
// Encrypt config
|
||||
if len(target.Config) > 0 {
|
||||
|
||||
@@ -169,6 +169,9 @@ export const getNotifications = (params: Record<string, string> = {}) => {
|
||||
return fetchJSON<PaginatedResponse<Notification>>(`${BASE}/notifications?${qs}`);
|
||||
};
|
||||
|
||||
export const getNotification = (id: string) =>
|
||||
fetchJSON<Notification>(`${BASE}/notifications/${id}`);
|
||||
|
||||
export const markNotificationRead = (id: string) =>
|
||||
fetchJSON<{ message: string }>(`${BASE}/notifications/${id}/read`, { method: 'POST' });
|
||||
|
||||
@@ -178,6 +181,9 @@ export const getAuditEvents = (params: Record<string, string> = {}) => {
|
||||
return fetchJSON<PaginatedResponse<AuditEvent>>(`${BASE}/audit?${qs}`);
|
||||
};
|
||||
|
||||
export const getAuditEvent = (id: string) =>
|
||||
fetchJSON<AuditEvent>(`${BASE}/audit/${id}`);
|
||||
|
||||
// Policies
|
||||
export const getPolicies = (params: Record<string, string> = {}) => {
|
||||
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
||||
|
||||
+8
-10
@@ -10,11 +10,11 @@ export interface Certificate {
|
||||
team_id: string;
|
||||
renewal_policy_id: string;
|
||||
certificate_profile_id: string;
|
||||
serial_number: string;
|
||||
fingerprint: string;
|
||||
key_algorithm: string;
|
||||
key_size: number;
|
||||
issued_at: string;
|
||||
serial_number?: string;
|
||||
fingerprint_sha256?: string;
|
||||
key_algorithm?: string;
|
||||
key_size?: number;
|
||||
issued_at?: string;
|
||||
expires_at: string;
|
||||
revoked_at?: string;
|
||||
revocation_reason?: string;
|
||||
@@ -40,11 +40,9 @@ export const REVOCATION_REASONS = [
|
||||
export interface CertificateVersion {
|
||||
id: string;
|
||||
certificate_id: string;
|
||||
version: number;
|
||||
serial_number: string;
|
||||
fingerprint: string;
|
||||
cert_pem: string;
|
||||
chain_pem: string;
|
||||
fingerprint_sha256: string;
|
||||
pem_chain: string;
|
||||
csr_pem: string;
|
||||
not_before: string;
|
||||
not_after: string;
|
||||
@@ -80,7 +78,7 @@ export interface Job {
|
||||
status: string;
|
||||
attempts: number;
|
||||
max_attempts: number;
|
||||
error_message: string;
|
||||
last_error?: string;
|
||||
scheduled_at: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function formatDate(iso: string): string {
|
||||
export function formatDate(iso: string | undefined | null): string {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export function formatDateTime(iso: string): string {
|
||||
export function formatDateTime(iso: string | undefined | null): string {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
@@ -29,19 +29,23 @@ export interface IssuerTypeConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical type label map. Keys match what the backend API returns.
|
||||
* DB stores: local, acme, stepca, openssl, VaultPKI, DigiCert
|
||||
* Canonical type label map. Keys MUST match backend IssuerType constants
|
||||
* defined in internal/domain/connector.go (e.g., "ACME", "GenericCA", "StepCA").
|
||||
*/
|
||||
export const typeLabels: Record<string, string> = {
|
||||
local: 'Local CA',
|
||||
GenericCA: 'Local CA',
|
||||
local: 'Local CA', // backward compat for old DB records
|
||||
local_ca: 'Local CA', // backward compat (some frontend references)
|
||||
acme: 'ACME',
|
||||
stepca: 'step-ca',
|
||||
openssl: 'OpenSSL/Custom',
|
||||
ACME: 'ACME',
|
||||
acme: 'ACME', // backward compat for old DB records
|
||||
StepCA: 'step-ca',
|
||||
stepca: 'step-ca', // backward compat for old DB records
|
||||
OpenSSL: 'OpenSSL/Custom',
|
||||
openssl: 'OpenSSL/Custom', // backward compat for old DB records
|
||||
VaultPKI: 'Vault PKI',
|
||||
DigiCert: 'DigiCert',
|
||||
Sectigo: 'Sectigo SCM',
|
||||
manual: 'Manual',
|
||||
GoogleCAS: 'Google CAS',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,7 @@ export const typeLabels: Record<string, string> = {
|
||||
*/
|
||||
export const issuerTypes: IssuerTypeConfig[] = [
|
||||
{
|
||||
id: 'acme',
|
||||
id: 'ACME',
|
||||
name: 'ACME',
|
||||
description: "Let's Encrypt, ZeroSSL, or any ACME-compatible CA",
|
||||
icon: '\uD83D\uDD12',
|
||||
@@ -64,7 +68,7 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'local',
|
||||
id: 'GenericCA',
|
||||
name: 'Local CA',
|
||||
description: 'Self-signed or subordinate CA for internal certificates',
|
||||
icon: '\uD83C\uDFE0',
|
||||
@@ -74,14 +78,15 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stepca',
|
||||
id: 'StepCA',
|
||||
name: 'step-ca',
|
||||
description: 'Smallstep private CA with JWK provisioner auth',
|
||||
icon: '\uD83D\uDC63',
|
||||
configFields: [
|
||||
{ key: 'ca_url', label: 'CA URL', placeholder: 'https://ca.example.com', required: true },
|
||||
{ key: 'provisioner_name', label: 'Provisioner Name', placeholder: 'my-provisioner', required: true },
|
||||
{ key: 'provisioner_key', label: 'Provisioner Key (JWK)', placeholder: '{...}', type: 'textarea', required: true, sensitive: true },
|
||||
{ key: 'provisioner_key_path', label: 'Provisioner Key Path', placeholder: '/path/to/provisioner.key', required: false, sensitive: true },
|
||||
{ key: 'provisioner_password', label: 'Provisioner Password', placeholder: 'Password for encrypted key', required: false, type: 'password', sensitive: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -110,7 +115,7 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'openssl',
|
||||
id: 'OpenSSL',
|
||||
name: 'OpenSSL/Custom',
|
||||
description: 'Script-based signing with your own CA',
|
||||
icon: '\uD83D\uDD27',
|
||||
@@ -188,7 +193,10 @@ export function getIssuerCatalogStatus(
|
||||
}
|
||||
// Match both the canonical id and common aliases
|
||||
const aliases: Record<string, string[]> = {
|
||||
local: ['local', 'local_ca'],
|
||||
GenericCA: ['GenericCA', 'local', 'local_ca'],
|
||||
ACME: ['ACME', 'acme'],
|
||||
StepCA: ['StepCA', 'stepca'],
|
||||
OpenSSL: ['OpenSSL', 'openssl'],
|
||||
};
|
||||
const matchIds = aliases[t.id] || [t.id];
|
||||
const matching = configuredIssuers.filter(i => matchIds.includes(i.type));
|
||||
|
||||
@@ -60,7 +60,7 @@ function TimelineStep({ label, status, time, isLast }: { label: string; status:
|
||||
);
|
||||
}
|
||||
|
||||
function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certId: string; certStatus: string; createdAt: string; issuedAt: string }) {
|
||||
function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certId: string; certStatus: string; createdAt: string; issuedAt?: string }) {
|
||||
const { data: jobsData } = useQuery({
|
||||
queryKey: ['jobs', { certificate_id: certId }],
|
||||
queryFn: () => getJobs({ certificate_id: certId }),
|
||||
@@ -372,6 +372,12 @@ export default function CertificateDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Derive certificate metadata from latest version (backend doesn't include these on the cert object)
|
||||
const latestVersion = versions?.data?.[0];
|
||||
const serialNumber = cert.serial_number || latestVersion?.serial_number;
|
||||
const fingerprintSha256 = cert.fingerprint_sha256 || latestVersion?.fingerprint_sha256;
|
||||
const issuedAt = cert.issued_at || latestVersion?.not_before;
|
||||
|
||||
const days = daysUntil(cert.expires_at);
|
||||
const isRevoked = cert.status === 'Revoked';
|
||||
const isArchived = cert.status === 'Archived';
|
||||
@@ -492,7 +498,7 @@ export default function CertificateDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Deployment Status Timeline */}
|
||||
<DeploymentTimeline certId={id!} certStatus={cert.status} createdAt={cert.created_at} issuedAt={cert.issued_at} />
|
||||
<DeploymentTimeline certId={id!} certStatus={cert.status} createdAt={cert.created_at} issuedAt={issuedAt} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Certificate Info */}
|
||||
@@ -518,9 +524,9 @@ export default function CertificateDetailPage() {
|
||||
})}
|
||||
</span>
|
||||
) : '—'} />
|
||||
<InfoRow label="Serial Number" value={cert.serial_number || '—'} />
|
||||
<InfoRow label="Serial Number" value={serialNumber || '—'} />
|
||||
<InfoRow label="Fingerprint" value={
|
||||
cert.fingerprint ? <span className="font-mono text-xs">{cert.fingerprint.slice(0, 24)}...</span> : '—'
|
||||
fingerprintSha256 ? <span className="font-mono text-xs">{fingerprintSha256.slice(0, 24)}...</span> : '—'
|
||||
} />
|
||||
<InfoRow label="Key Algorithm" value={cert.key_algorithm || '—'} />
|
||||
<InfoRow label="Key Size" value={cert.key_size ? `${cert.key_size} bits` : '—'} />
|
||||
@@ -556,7 +562,7 @@ export default function CertificateDetailPage() {
|
||||
{/* Lifecycle */}
|
||||
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-ink-muted mb-4">Lifecycle</h3>
|
||||
<InfoRow label="Issued" value={formatDate(cert.issued_at)} />
|
||||
<InfoRow label="Issued" value={formatDate(issuedAt)} />
|
||||
<InfoRow label="Expires" value={
|
||||
<span className={isRevoked ? 'text-red-600 line-through' : expiryColor(days)}>
|
||||
{formatDate(cert.expires_at)} ({days <= 0 ? 'expired' : `${days} days`})
|
||||
@@ -615,7 +621,7 @@ export default function CertificateDetailPage() {
|
||||
<div key={v.id} className="flex items-center justify-between py-2 border-b border-surface-border/50 last:border-0">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-ink">Version {v.version}</span>
|
||||
<span className="text-sm text-ink">Version {versions.data.length - idx}</span>
|
||||
{idx === 0 && <span className="text-xs bg-brand-100 text-brand-700 px-1.5 py-0.5 rounded">Current</span>}
|
||||
</div>
|
||||
<div className="text-xs text-ink-faint font-mono">{v.serial_number}</div>
|
||||
|
||||
@@ -114,9 +114,9 @@ export default function JobDetailPage() {
|
||||
} />
|
||||
)}
|
||||
<InfoRow label="Attempts" value={`${job.attempts} / ${job.max_attempts}`} />
|
||||
{job.error_message && (
|
||||
{job.last_error && (
|
||||
<InfoRow label="Error" value={
|
||||
<span className="text-red-600 text-xs">{job.error_message}</span>
|
||||
<span className="text-red-600 text-xs">{job.last_error}</span>
|
||||
} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -139,9 +139,9 @@ export default function JobsPage() {
|
||||
{
|
||||
key: 'error',
|
||||
label: 'Error',
|
||||
render: (j) => j.status === 'Failed' && j.error_message ? (
|
||||
<span className="text-xs text-red-600 truncate max-w-[200px] inline-block" title={j.error_message}>
|
||||
{j.error_message.length > 80 ? j.error_message.substring(0, 80) + '...' : j.error_message}
|
||||
render: (j) => j.status === 'Failed' && j.last_error ? (
|
||||
<span className="text-xs text-red-600 truncate max-w-[200px] inline-block" title={j.last_error}>
|
||||
{j.last_error.length > 80 ? j.last_error.substring(0, 80) + '...' : j.last_error}
|
||||
</span>
|
||||
) : <span className="text-xs text-ink-faint">—</span>,
|
||||
},
|
||||
|
||||
@@ -231,7 +231,7 @@ export default function TargetDetailPage() {
|
||||
{target.config && Object.keys(target.config).length > 0 ? (
|
||||
<div className="space-y-0">
|
||||
{Object.entries(target.config).map(([key, val]) => {
|
||||
const sensitiveKeys = ['password', 'secret', 'token', 'key', 'winrm_password', 'keystore_password'];
|
||||
const sensitiveKeys = ['password', 'secret', 'token', 'key', 'passphrase', 'winrm_password', 'keystore_password'];
|
||||
const isSensitive = sensitiveKeys.some(s => key.toLowerCase().includes(s));
|
||||
const displayVal = isSensitive && val ? '********' : String(val);
|
||||
return (
|
||||
|
||||
@@ -47,18 +47,20 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/nginx/ssl/cert.pem', required: true },
|
||||
{ key: 'key_path', label: 'Key Path', placeholder: '/etc/nginx/ssl/key.pem', required: true },
|
||||
{ key: 'chain_path', label: 'Chain Path', placeholder: '/etc/nginx/ssl/chain.pem' },
|
||||
{ key: 'reload_cmd', label: 'Reload Command', placeholder: 'nginx -t && systemctl reload nginx' },
|
||||
{ key: 'reload_command', label: 'Reload Command', placeholder: 'nginx -s reload' },
|
||||
{ key: 'validate_command', label: 'Validate Command', placeholder: 'nginx -t' },
|
||||
],
|
||||
Apache: [
|
||||
{ key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/apache2/ssl/cert.pem', required: true },
|
||||
{ key: 'key_path', label: 'Key Path', placeholder: '/etc/apache2/ssl/key.pem', required: true },
|
||||
{ key: 'chain_path', label: 'Chain Path', placeholder: '/etc/apache2/ssl/chain.pem' },
|
||||
{ key: 'reload_cmd', label: 'Reload Command', placeholder: 'apachectl configtest && apachectl graceful' },
|
||||
{ key: 'reload_command', label: 'Reload Command', placeholder: 'apachectl graceful' },
|
||||
{ key: 'validate_command', label: 'Validate Command', placeholder: 'apachectl configtest' },
|
||||
],
|
||||
HAProxy: [
|
||||
{ key: 'pem_path', label: 'Combined PEM Path', placeholder: '/etc/haproxy/certs/combined.pem', required: true },
|
||||
{ key: 'reload_cmd', label: 'Reload Command', placeholder: 'systemctl reload haproxy' },
|
||||
{ key: 'validate_cmd', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' },
|
||||
{ key: 'reload_command', label: 'Reload Command', placeholder: 'systemctl reload haproxy' },
|
||||
{ key: 'validate_command', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' },
|
||||
],
|
||||
Traefik: [
|
||||
{ key: 'cert_dir', label: 'Certificate Directory', placeholder: '/etc/traefik/certs', required: true },
|
||||
@@ -87,6 +89,7 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'validate_command', label: 'Validate Command', placeholder: 'postfix check' },
|
||||
],
|
||||
Dovecot: [
|
||||
{ key: 'mode', label: 'Mode', placeholder: 'dovecot (auto-set)' },
|
||||
{ key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/dovecot/certs/cert.pem' },
|
||||
{ key: 'key_path', label: 'Key Path', placeholder: '/etc/dovecot/certs/key.pem' },
|
||||
{ key: 'chain_path', label: 'Chain Path (optional)', placeholder: '/etc/dovecot/certs/chain.pem' },
|
||||
@@ -104,6 +107,7 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'timeout', label: 'Timeout (seconds)', placeholder: '30' },
|
||||
],
|
||||
IIS: [
|
||||
{ key: 'hostname', label: 'Target Hostname', placeholder: 'iis-server.example.com' },
|
||||
{ key: 'site_name', label: 'IIS Site Name', placeholder: 'Default Web Site', required: true },
|
||||
{ key: 'cert_store', label: 'Certificate Store', placeholder: 'My', required: true },
|
||||
{ key: 'port', label: 'HTTPS Port', placeholder: '443' },
|
||||
@@ -111,12 +115,13 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'binding_info', label: 'Host Header (SNI)', placeholder: 'www.example.com' },
|
||||
{ key: 'sni', label: 'Enable SNI', placeholder: 'true or false' },
|
||||
{ key: 'mode', label: 'Deployment Mode', placeholder: 'local (default) or winrm' },
|
||||
{ key: 'winrm.winrm_host', label: 'WinRM Host (remote mode)', placeholder: 'iis-server.example.com' },
|
||||
{ key: 'winrm.winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' },
|
||||
{ key: 'winrm.winrm_username', label: 'WinRM Username', placeholder: 'Administrator' },
|
||||
{ key: 'winrm.winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' },
|
||||
{ key: 'winrm.winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' },
|
||||
{ key: 'winrm.winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' },
|
||||
{ key: 'winrm_host', label: 'WinRM Host (remote mode)', placeholder: 'iis-server.example.com' },
|
||||
{ key: 'winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' },
|
||||
{ key: 'winrm_username', label: 'WinRM Username', placeholder: 'Administrator' },
|
||||
{ key: 'winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' },
|
||||
{ key: 'winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' },
|
||||
{ key: 'winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' },
|
||||
{ key: 'winrm_timeout', label: 'WinRM Timeout (seconds)', placeholder: '60' },
|
||||
],
|
||||
SSH: [
|
||||
{ key: 'host', label: 'SSH Host', placeholder: '192.168.1.100 or server.example.com', required: true },
|
||||
@@ -124,10 +129,14 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'user', label: 'SSH Username', placeholder: 'root or certctl', required: true },
|
||||
{ key: 'auth_method', label: 'Auth Method', placeholder: 'key (default) or password' },
|
||||
{ key: 'private_key_path', label: 'Private Key Path', placeholder: '/home/certctl/.ssh/id_ed25519' },
|
||||
{ key: 'private_key', label: 'Inline Private Key PEM', placeholder: 'Paste PEM key (alternative to path)' },
|
||||
{ key: 'password', label: 'SSH Password', placeholder: 'Leave empty for key auth' },
|
||||
{ key: 'passphrase', label: 'Key Passphrase', placeholder: 'For encrypted private keys' },
|
||||
{ key: 'cert_path', label: 'Remote Certificate Path', placeholder: '/etc/ssl/certs/cert.pem', required: true },
|
||||
{ key: 'key_path', label: 'Remote Key Path', placeholder: '/etc/ssl/private/key.pem', required: true },
|
||||
{ key: 'chain_path', label: 'Remote Chain Path (optional)', placeholder: '/etc/ssl/certs/chain.pem' },
|
||||
{ key: 'cert_mode', label: 'Cert File Permissions', placeholder: '0644 (default)' },
|
||||
{ key: 'key_mode', label: 'Key File Permissions', placeholder: '0600 (default)' },
|
||||
{ key: 'reload_command', label: 'Reload Command (optional)', placeholder: 'systemctl reload nginx' },
|
||||
{ key: 'timeout', label: 'Connection Timeout (seconds)', placeholder: '30 (default)' },
|
||||
],
|
||||
@@ -141,12 +150,15 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ key: 'winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' },
|
||||
{ key: 'winrm_username', label: 'WinRM Username', placeholder: 'Administrator' },
|
||||
{ key: 'winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' },
|
||||
{ key: 'winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' },
|
||||
{ key: 'winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' },
|
||||
],
|
||||
JavaKeystore: [
|
||||
{ key: 'keystore_path', label: 'Keystore Path', placeholder: '/opt/app/conf/keystore.p12', required: true },
|
||||
{ key: 'keystore_password', label: 'Keystore Password', placeholder: 'changeit', required: true },
|
||||
{ key: 'keystore_type', label: 'Keystore Type', placeholder: 'PKCS12 (default) or JKS' },
|
||||
{ key: 'alias', label: 'Key Alias', placeholder: 'server (default)' },
|
||||
{ key: 'create_keystore', label: 'Create Keystore If Missing', placeholder: 'true (default)' },
|
||||
{ key: 'reload_command', label: 'Reload Command (optional)', placeholder: 'systemctl restart tomcat' },
|
||||
{ key: 'keytool_path', label: 'Keytool Path (optional)', placeholder: 'keytool (default, from PATH)' },
|
||||
],
|
||||
@@ -160,12 +172,64 @@ function CreateTargetWizard({ onClose, onSuccess }: { onClose: () => void; onSuc
|
||||
const [config, setConfig] = useState<Record<string, string>>({});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Fields that backends expect as boolean (Go bool)
|
||||
const BOOL_FIELDS = new Set([
|
||||
'sni', 'insecure', 'sds_config', 'remove_expired', 'create_keystore',
|
||||
'winrm_https', 'winrm_insecure',
|
||||
]);
|
||||
// Fields that backends expect as integer (Go int)
|
||||
const INT_FIELDS = new Set([
|
||||
'port', 'timeout', 'winrm_port', 'winrm_timeout', 'timeout_seconds',
|
||||
]);
|
||||
|
||||
// Coerce string form values to their Go types
|
||||
const coerceValue = (key: string, val: string): unknown => {
|
||||
if (BOOL_FIELDS.has(key)) return val === 'true';
|
||||
if (INT_FIELDS.has(key)) { const n = parseInt(val, 10); return isNaN(n) ? val : n; }
|
||||
return val;
|
||||
};
|
||||
|
||||
// Build config payload with type-specific transformations
|
||||
const buildConfigPayload = () => {
|
||||
const flat = Object.fromEntries(Object.entries(config).filter(([, v]) => v));
|
||||
|
||||
// Dovecot uses the same Postfix connector with mode="dovecot"
|
||||
if (targetType === 'Dovecot' && !flat['mode']) {
|
||||
flat['mode'] = 'dovecot';
|
||||
}
|
||||
|
||||
// IIS backend expects WinRM fields nested under "winrm" key
|
||||
if (targetType === 'IIS') {
|
||||
const iisWinrmKeys = ['winrm_host', 'winrm_port', 'winrm_username', 'winrm_password', 'winrm_https', 'winrm_insecure', 'winrm_timeout'];
|
||||
const winrmObj: Record<string, unknown> = {};
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(flat)) {
|
||||
if (iisWinrmKeys.includes(k)) {
|
||||
winrmObj[k] = coerceValue(k, v);
|
||||
} else {
|
||||
result[k] = coerceValue(k, v);
|
||||
}
|
||||
}
|
||||
if (Object.keys(winrmObj).length > 0) {
|
||||
result['winrm'] = winrmObj;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// All other target types: coerce values to proper Go types
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(flat)) {
|
||||
result[k] = coerceValue(k, v);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => createTarget({
|
||||
name,
|
||||
type: targetType,
|
||||
agent_id: agentId,
|
||||
config: Object.fromEntries(Object.entries(config).filter(([, v]) => v)),
|
||||
config: buildConfigPayload(),
|
||||
}),
|
||||
onSuccess: () => onSuccess(),
|
||||
onError: (err: Error) => setError(err.message),
|
||||
|
||||
Reference in New Issue
Block a user