Compare commits

..

2 Commits

Author SHA1 Message Date
shankar0123 a0b9285323 fix(gui): add missing Name field to certificate creation form
The New Certificate modal was missing the required "name" field,
causing all certificate creation attempts to fail with "name is
required". Added Name text input above ID field with client-side
validation matching the backend requirement.

Fixes #GH-issue (name is required on certificate creation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 07:53:14 -04:00
shankar0123 2655493ac8 fix(docs): correct migration guides — 17 issues found via repo audit
Fixes factual errors, broken links, wrong ports, inaccurate GUI
descriptions, and misleading config formats across all three migration
guides (certbot, acme.sh, cert-manager).

Key fixes:
- Correct server port from 8080/3000 to 8443 across all guides
- Fix HTTPS→HTTP for Docker Compose (not TLS-terminated)
- Fix heartbeat interval: 60 seconds, not 5 minutes
- Fix "50 servers" → "10 servers" (50 certs across 10 servers)
- Replace JSON config blocks with env var format (actual config method)
- Fix policy creation flow to match actual GUI (name/type/severity/config)
- Fix issuer wizard description to match actual 2-step flow
- Fix Vault PKI "coming in v2.1" → "planned" (ships post-2.1.0)
- Fix 5 broken links (cert-manager.md, quickstart anchors, architecture anchor)
- Remove claim of auto-generated suggestions in discovery flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 01:34:22 -04:00
4 changed files with 81 additions and 57 deletions
+14 -13
View File
@@ -27,7 +27,7 @@ Result:
Deploy certctl control plane once (Docker Compose, Kubernetes Helm chart, or self-hosted). Deploy agents on your VMs, bare metal, and network appliances. One dashboard shows: Deploy certctl control plane once (Docker Compose, Kubernetes Helm chart, or self-hosted). Deploy agents on your VMs, bare metal, and network appliances. One dashboard shows:
- **All cert-manager certs** via discovery scanning (agents find cert-manager-issued certs copied to target machines, or scan the cluster directly) - **All cert-manager certs** via discovery scanning (agents find cert-manager-issued certs copied to target machines, or scan the cluster directly)
- **All certctl-managed certs** issued by shared issuers (ACME, step-ca, Vault PKI (coming in v2.1), private CA) - **All certctl-managed certs** issued by shared issuers (ACME, step-ca, Vault PKI (planned), private CA)
- **Unified renewal and deployment** across both worlds - **Unified renewal and deployment** across both worlds
- **Single pane of glass** with expiration timeline, renewal status, deployment verification, audit trail - **Single pane of glass** with expiration timeline, renewal status, deployment verification, audit trail
@@ -39,8 +39,7 @@ Deploy certctl control plane once (Docker Compose, Kubernetes Helm chart, or sel
```bash ```bash
cd /opt/certctl cd /opt/certctl
docker compose up -d docker compose up -d
# Dashboard: http://localhost:3000 # Dashboard & API: http://localhost:8443
# API: http://localhost:8080
``` ```
**Option B: Kubernetes** (recommended for prod) **Option B: Kubernetes** (recommended for prod)
@@ -60,7 +59,7 @@ chmod +x /usr/local/bin/certctl-agent
# Config # Config
sudo tee /etc/certctl/agent.env > /dev/null <<EOF sudo tee /etc/certctl/agent.env > /dev/null <<EOF
CERTCTL_SERVER_URL=https://certctl-control-plane:8080 CERTCTL_SERVER_URL=http://certctl-control-plane:8443
CERTCTL_API_KEY=your-api-key CERTCTL_API_KEY=your-api-key
CERTCTL_DISCOVERY_DIRS=/etc/nginx/certs,/etc/ssl,/etc/letsencrypt/live CERTCTL_DISCOVERY_DIRS=/etc/nginx/certs,/etc/ssl,/etc/letsencrypt/live
CERTCTL_KEY_DIR=/var/lib/certctl/keys CERTCTL_KEY_DIR=/var/lib/certctl/keys
@@ -83,18 +82,20 @@ Agents scan configured directories and report back all existing certs. In the da
Set up the same issuer certctl uses for non-Kubernetes certs: Set up the same issuer certctl uses for non-Kubernetes certs:
- **ACME** (Let's Encrypt, for public certs) - **ACME** (Let's Encrypt, for public certs)
- **step-ca** (Smallstep, for internal certs) - **step-ca** (Smallstep, for internal certs)
- **Vault PKI** (coming in v2.1) (HashiCorp Vault, for enterprise PKI) - **Vault PKI** (planned) (HashiCorp Vault, for enterprise PKI)
- **Private CA** (your own internal root CA) - **Private CA** (your own internal root CA)
No new CA infrastructure needed. If cert-manager already uses your CA, certctl points to the same one. No new CA infrastructure needed. If cert-manager already uses your CA, certctl points to the same one.
### 5. Create Policies for Non-Kubernetes Certs ### 5. Create Policies for Non-Kubernetes Certs
Go to **Policies****New Policy**: Go to **Policies****+ New Policy** to create enforcement rules:
- Issuer: shared (ACME, step-ca, Vault (coming in v2.1), private CA) - **Name:** e.g., "VM Certificate Policy"
- Profile: serverAuth for NGINX/Apache/HAProxy, clientAuth for mTLS, emailProtection for S/MIME - **Type:** `expiration_window` or `key_algorithm` (enforce renewal thresholds or crypto requirements)
- Renewal Threshold: 30 days (default, adjust per SLA) - **Severity:** `high`
- Scope: agent groups (VMs, bare metal, appliances) - **Config:** set your enforcement parameters
Certificates are linked to issuers and profiles when created or claimed from discovery. Policies add guardrails — enforcing key algorithm requirements, expiration windows, and other compliance rules across your fleet.
### 6. View Unified Inventory ### 6. View Unified Inventory
@@ -114,7 +115,7 @@ Go to **Policies** → **New Policy**:
If cert-manager and certctl both use the same CA: If cert-manager and certctl both use the same CA:
- **ACME**: cert-manager uses ClusterIssuer + certctl uses ACME connector → same Let's Encrypt account, transparent coexistence - **ACME**: cert-manager uses ClusterIssuer + certctl uses ACME connector → same Let's Encrypt account, transparent coexistence
- **step-ca**: cert-manager uses external issuer CRD + certctl uses step-ca connector → same provisioner, shared certificate inventory - **step-ca**: cert-manager uses external issuer CRD + certctl uses step-ca connector → same provisioner, shared certificate inventory
- **Vault PKI** (coming in v2.1): cert-manager uses external issuer CRD + certctl uses Vault connector → same mount, same audit trail - **Vault PKI** (planned): cert-manager uses external issuer CRD + certctl uses Vault connector → same mount, same audit trail
No conflict. They just issue certs through the same CA. certctl's discovery scanning finds cert-manager-issued certs and shows them alongside certctl-managed ones. No conflict. They just issue certs through the same CA. certctl's discovery scanning finds cert-manager-issued certs and shows them alongside certctl-managed ones.
@@ -138,6 +139,6 @@ For now: cert-manager handles Kubernetes, certctl handles everything else. They
## Next Steps ## Next Steps
1. Review [Quick Start](./quickstart.md) for a 5-minute demo 1. Review [Quick Start](./quickstart.md) for a 5-minute demo
2. Explore [Agents and Targets](./architecture.md#agents-and-targets) for deployment architecture 2. Explore [Architecture](./architecture.md#agents) for deployment architecture
3. Read about [Discovery Scanning](./quickstart.md#discovery) to auto-find certs 3. Read about [Discovery Scanning](./quickstart.md#certificate-discovery) to auto-find certs
4. Check [Helm Chart](../deploy/helm/certctl/) for production Kubernetes deployment 4. Check [Helm Chart](../deploy/helm/certctl/) for production Kubernetes deployment
+30 -24
View File
@@ -99,18 +99,23 @@ Environment="CERTCTL_DISCOVERY_DIRS=/etc/acme.sh"
In the **Discovery** page: In the **Discovery** page:
1. Review the "Unmanaged" certificates found by the agent 1. Review the "Unmanaged" certificates found by the agent
2. Click **Claim** on each acme.sh certificate 2. Click **Claim** on each acme.sh certificate
3. Map to the certificate ID (certctl auto-generates suggestions) 3. Enter the managed certificate ID to link it (e.g., `mc-api-prod`)
Once claimed, the certificate appears in the main **Certificates** page with ownership, renewal history, and deployment status. Once claimed, the certificate appears in the main **Certificates** page with ownership, renewal history, and deployment status.
### 5. Create an ACME Issuer ### 5. Create an ACME Issuer
In **Issuers****Configure New Issuer:** In **Issuers****+ New Issuer:**
- **Type:** ACME v2 1. Select **ACME** from the issuer type grid
- **Directory URL:** `https://acme-v02.api.letsencrypt.org/directory` (production) or staging for testing 2. Fill in the type-specific fields: name, directory URL (`https://acme-v02.api.letsencrypt.org/directory`), and config
- **Email:** Same email as your acme.sh account (required for ACME ToS)
- **Challenge Type:** DNS-01 (to match acme.sh's DNS validation) Or configure via environment variables:
```bash
export CERTCTL_ACME_DIRECTORY_URL=https://acme-v02.api.letsencrypt.org/directory
export CERTCTL_ACME_EMAIL=your-email@example.com # same as your acme.sh account
export CERTCTL_ACME_CHALLENGE_TYPE=dns-01
```
### 6. Adapt Your DNS Provider Scripts ### 6. Adapt Your DNS Provider Scripts
@@ -182,26 +187,28 @@ curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_record
-H "X-Auth-Key: ${CF_KEY}" -H "X-Auth-Key: ${CF_KEY}"
``` ```
Configure in the ACME issuer: Configure the ACME issuer via environment variables:
```json ```bash
{ export CERTCTL_ACME_DIRECTORY_URL=https://acme-v02.api.letsencrypt.org/directory
"challenge_type": "dns-01", export CERTCTL_ACME_EMAIL=your-email@example.com
"dns_present_script": "/etc/certctl/dns/cloudflare-present.sh", export CERTCTL_ACME_CHALLENGE_TYPE=dns-01
"dns_cleanup_script": "/etc/certctl/dns/cloudflare-cleanup.sh", export CERTCTL_ACME_DNS_PRESENT_SCRIPT=/etc/certctl/dns/cloudflare-present.sh
"dns_propagation_wait": 30 export CERTCTL_ACME_DNS_CLEANUP_SCRIPT=/etc/certctl/dns/cloudflare-cleanup.sh
}
``` ```
Or create the issuer through the dashboard: **Issuers****+ New Issuer** → select **ACME** → fill in the config fields.
### 7. Create Renewal Policies ### 7. Create Renewal Policies
In **Policies:** In **Policies****+ New Policy:**
- **Certificate Profile:** Select the issuer and challenge type from step 5 - **Name:** e.g., "ACME DNS-01 Policy"
- **Renewal Threshold:** 30 days before expiry (or match your acme.sh cron settings) - **Type:** `expiration_window` (enforces renewal thresholds)
- **Agent Group:** Select which agents should renew certificates - **Severity:** `high`
- **Config:** set your renewal window (default: 30 days before expiry)
Set one policy per domain or domain pattern. Renewal scheduling is driven by the certificate's assigned profile and issuer. Policies add enforcement guardrails on top.
### 8. Phase Out acme.sh Cron ### 8. Phase Out acme.sh Cron
@@ -252,11 +259,10 @@ Benefits:
To enable: To enable:
```json ```bash
{ export CERTCTL_ACME_CHALLENGE_TYPE=dns-persist-01
"challenge_type": "dns-persist-01", export CERTCTL_ACME_DNS_PERSIST_ISSUER_DOMAIN=letsencrypt.org
"dns_persist_issuer_domain": "acme-v02.api.letsencrypt.org" export CERTCTL_ACME_DNS_PRESENT_SCRIPT=/etc/certctl/dns/cloudflare-present.sh
}
``` ```
certctl automatically falls back to DNS-01 if the CA doesn't support dns-persist-01 yet. certctl automatically falls back to DNS-01 if the CA doesn't support dns-persist-01 yet.
+29 -19
View File
@@ -22,7 +22,7 @@ Option A: Docker Compose (quickest for evaluation)
```bash ```bash
cd /opt/certctl cd /opt/certctl
docker compose up -d docker compose up -d
# Dashboard & API: https://localhost:8443 # Dashboard & API: http://localhost:8443
# Default API key in logs (grep CERTCTL_API_KEY docker logs certctl-server) # Default API key in logs (grep CERTCTL_API_KEY docker logs certctl-server)
``` ```
@@ -45,7 +45,7 @@ chmod +x /usr/local/bin/certctl-agent
# Create config # Create config
sudo mkdir -p /etc/certctl /var/lib/certctl/keys sudo mkdir -p /etc/certctl /var/lib/certctl/keys
sudo tee /etc/certctl/agent.env > /dev/null <<EOF sudo tee /etc/certctl/agent.env > /dev/null <<EOF
CERTCTL_SERVER_URL=https://certctl-control-plane.example.com:8080 CERTCTL_SERVER_URL=http://certctl-control-plane.example.com:8443
CERTCTL_API_KEY=your-api-key-here CERTCTL_API_KEY=your-api-key-here
CERTCTL_DISCOVERY_DIRS=/etc/letsencrypt/live CERTCTL_DISCOVERY_DIRS=/etc/letsencrypt/live
CERTCTL_KEY_DIR=/var/lib/certctl/keys CERTCTL_KEY_DIR=/var/lib/certctl/keys
@@ -71,24 +71,34 @@ The control plane now knows about all 50 certs and where they live.
### 4. Configure ACME Issuer ### 4. Configure ACME Issuer
Go to **Issuers****Add Issuer**: Go to **Issuers****+ New Issuer**:
- Type: ACME 1. Select **ACME** from the issuer type grid
- Directory URL: `https://acme-v02.api.letsencrypt.org/directory` (production) 2. Fill in the type-specific fields: name, directory URL (`https://acme-v02.api.letsencrypt.org/directory`), and any required config
- Email: your Let's Encrypt account email
- Challenge Type: `http-01` (if you have HTTP access) or `dns-01` (for wildcard/internal certs)
- For DNS-01, provide your DNS provider's script hook (Cloudflare, Route53, Azure DNS, etc.)
Test the connection. certctl uses the same Let's Encrypt account; no new credentials needed. Alternatively, configure via environment variables before starting the server:
```bash
export CERTCTL_ACME_DIRECTORY_URL=https://acme-v02.api.letsencrypt.org/directory
export CERTCTL_ACME_EMAIL=your-email@example.com
export CERTCTL_ACME_CHALLENGE_TYPE=http-01 # or dns-01 for wildcard certs
```
For DNS-01, also set:
```bash
export CERTCTL_ACME_DNS_PRESENT_SCRIPT=/etc/certctl/dns/present.sh
export CERTCTL_ACME_DNS_CLEANUP_SCRIPT=/etc/certctl/dns/cleanup.sh
```
certctl uses the same Let's Encrypt account; no new credentials needed.
### 5. Create Renewal Policies ### 5. Create Renewal Policies
Go to **Policies****New Policy**: Go to **Policies****+ New Policy** to create enforcement rules:
- Profile: ACME (or create a new one with `serverAuth` EKU) - Name: e.g., "ACME Renewal Policy"
- Issuer: the ACME issuer you just created - Type: `expiration_window` (to enforce renewal thresholds)
- Renewal Threshold: 30 days before expiry (default, adjust as needed) - Severity: `high`
- Scope: select agent groups or individual agents managing your servers - Config: set your renewal threshold (default: 30 days before expiry)
Assign this policy to your discovered certs. Renewal scheduling is driven by the certificate's assigned profile and issuer. Policies add enforcement guardrails (key algorithm requirements, expiration windows, etc.).
### 6. Disable Certbot Cron, One Server at a Time ### 6. Disable Certbot Cron, One Server at a Time
@@ -133,11 +143,11 @@ docker compose up -d
# Other options: CERTCTL_TEAMS_WEBHOOK_URL, CERTCTL_PAGERDUTY_ROUTING_KEY, CERTCTL_OPSGENIE_API_KEY # Other options: CERTCTL_TEAMS_WEBHOOK_URL, CERTCTL_PAGERDUTY_ROUTING_KEY, CERTCTL_OPSGENIE_API_KEY
``` ```
Now you get 30/14/7-day warnings before any cert expires, across all 50 servers, in one place. Now you get 30/14/7-day warnings before any cert expires, across all 10 servers, in one place.
## What Changes ## What Changes
- **Renewal**: Agent polls certctl for work instead of Certbot cron triggering locally. Faster failure detection (agent heartbeat every 5 minutes vs. cron running once a day). - **Renewal**: Agent polls certctl for work instead of Certbot cron triggering locally. Faster failure detection (agent heartbeat every 60 seconds vs. cron running once a day).
- **Deployment**: certctl verifies post-deployment by probing the live TLS endpoint and comparing SHA-256 fingerprints. Catches reload failures silently. - **Deployment**: certctl verifies post-deployment by probing the live TLS endpoint and comparing SHA-256 fingerprints. Catches reload failures silently.
- **Audit Trail**: Every renewal, deployment, and alert is logged immutably. Answer "who renewed cert X when and why" within seconds. - **Audit Trail**: Every renewal, deployment, and alert is logged immutably. Answer "who renewed cert X when and why" within seconds.
- **Alerting**: Threshold-based alerts to Slack/email/webhook 30/14/7 days before expiry, not when cert expires. - **Alerting**: Threshold-based alerts to Slack/email/webhook 30/14/7 days before expiry, not when cert expires.
@@ -157,5 +167,5 @@ certctl will stop renewing that cert when the policy is disabled. Certbot resume
## Next Steps ## Next Steps
- Review the [Concepts Guide](./concepts.md) for terminology (profiles, policies, agents, jobs) - Review the [Concepts Guide](./concepts.md) for terminology (profiles, policies, agents, jobs)
- Explore [Network Discovery](./quickstart.md#network-discovery) to find certificates you didn't know about - Explore [Network Discovery](./quickstart.md#network-discovery-agentless) to find certificates you didn't know about
- Set up [Kubernetes cert-manager integration](./cert-manager.md) if you manage in-cluster certs too - Set up [Kubernetes cert-manager integration](./certctl-for-cert-manager-users.md) if you manage in-cluster certs too
+8 -1
View File
@@ -13,6 +13,7 @@ import type { Certificate } from '../api/types';
function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const [form, setForm] = useState({ const [form, setForm] = useState({
name: '',
id: '', id: '',
common_name: '', common_name: '',
environment: 'production', environment: 'production',
@@ -35,6 +36,12 @@ function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; o
<h2 className="text-lg font-semibold text-ink mb-4">New Certificate</h2> <h2 className="text-lg font-semibold text-ink mb-4">New Certificate</h2>
{error && <div className="bg-red-50 border border-red-200 text-red-700 rounded px-3 py-2 text-sm mb-4">{error}</div>} {error && <div className="bg-red-50 border border-red-200 text-red-700 rounded px-3 py-2 text-sm mb-4">{error}</div>}
<div className="space-y-3"> <div className="space-y-3">
<div>
<label className="text-xs text-ink-muted block mb-1">Name *</label>
<input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400 focus:ring-1 focus:ring-brand-400/20"
placeholder="API Production Cert" />
</div>
<div> <div>
<label className="text-xs text-ink-muted block mb-1">ID (optional)</label> <label className="text-xs text-ink-muted block mb-1">ID (optional)</label>
<input value={form.id} onChange={e => setForm(f => ({ ...f, id: e.target.value }))} <input value={form.id} onChange={e => setForm(f => ({ ...f, id: e.target.value }))}
@@ -89,7 +96,7 @@ function CreateCertificateModal({ onClose, onSuccess }: { onClose: () => void; o
<button onClick={onClose} className="btn btn-ghost text-sm">Cancel</button> <button onClick={onClose} className="btn btn-ghost text-sm">Cancel</button>
<button <button
onClick={() => mutation.mutate()} onClick={() => mutation.mutate()}
disabled={!form.common_name || !form.issuer_id || mutation.isPending} disabled={!form.name || !form.common_name || !form.issuer_id || mutation.isPending}
className="btn btn-primary text-sm disabled:opacity-50" className="btn btn-primary text-sm disabled:opacity-50"
> >
{mutation.isPending ? 'Creating...' : 'Create Certificate'} {mutation.isPending ? 'Creating...' : 'Create Certificate'}