Runtime fixes: - Fix env var mismatch (CERTCTL_DB_URL → CERTCTL_DATABASE_URL) - Fix table name mismatches (certificates → managed_certificates, notifications → notification_events) - Add renewal_policy_id to certificate queries - Remove non-existent created_at from notification queries - Add env var fallback for agent CLI flags - Graceful degradation for missing notifiers/issuers in demo mode - Copy web/ directory in Dockerfile for dashboard serving Service layer: - Implement handler-service interface pattern across all services - Wire up certificate, agent, job, policy, team, owner, audit, notification services Documentation: - Add concepts.md: beginner-friendly guide to TLS, CAs, private keys - Rewrite quickstart.md with accurate API examples matching actual handlers - Add demo-advanced.md: interactive demo with cert issuance and automated script - Update architecture.md with correct table names and connector interfaces - Update connectors.md to match actual Go interface signatures - Update demo-guide.md with cross-references to new docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
Advanced Demo: Certificate Lifecycle End-to-End
This demo goes beyond browsing pre-loaded data. You'll create a team, register an owner, set up an issuer, create a certificate, trigger renewal, and watch everything appear in the dashboard in real time. By the end, you'll understand the full certificate lifecycle as certctl manages it.
Time: 10-15 minutes Prerequisites: certctl running via Docker Compose (see Quick Start)
Setup
Make sure certctl is running:
docker compose -f deploy/docker-compose.yml up -d
# Wait for healthy status
docker compose -f deploy/docker-compose.yml ps
Open http://localhost:8443 in your browser alongside your terminal. You'll watch changes appear in the dashboard as you make API calls.
Set up a base variable for convenience:
API="http://localhost:8443"
Part 1: Build the Organization Structure
Create a new team
curl -s -X POST $API/api/v1/teams \
-H "Content-Type: application/json" \
-d '{
"id": "t-demo",
"name": "Demo Team",
"description": "Team created during advanced demo walkthrough"
}' | jq .
Register an owner
curl -s -X POST $API/api/v1/owners \
-H "Content-Type: application/json" \
-d '{
"id": "o-demo-user",
"name": "Demo User",
"email": "demo@example.com",
"team_id": "t-demo"
}' | jq .
Verify both exist:
curl -s $API/api/v1/teams/t-demo | jq .
curl -s $API/api/v1/owners/o-demo-user | jq .
Part 2: Configure the Issuer
The demo ships with a Local CA issuer (iss-local) that can sign certificates immediately — no external CA needed. Let's verify it's available:
curl -s $API/api/v1/issuers/iss-local | jq .
You should see:
{
"id": "iss-local",
"name": "Local Dev CA",
"type": "GenericCA",
"enabled": true
}
This Local CA generates real X.509 certificates using Go's crypto/x509 library. The certificates are self-signed (not trusted by browsers in production), but structurally identical to production certificates — they have serial numbers, validity periods, SANs, key usage extensions, and a proper certificate chain.
Part 3: Create a Managed Certificate
Now the main event. Let's create a certificate for a fictional internal API:
curl -s -X POST $API/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"id": "mc-demo-api",
"name": "Demo API Certificate",
"common_name": "demo-api.internal.example.com",
"sans": ["demo-api.internal.example.com", "demo-api-v2.internal.example.com"],
"environment": "staging",
"owner_id": "o-demo-user",
"team_id": "t-demo",
"issuer_id": "iss-local",
"renewal_policy_id": "rp-default",
"status": "Pending",
"tags": {
"service": "demo-api",
"created_by": "advanced-demo",
"tier": "internal"
}
}' | jq .
Check the dashboard now. Click "Certificates" in the sidebar. You'll see your new "Demo API Certificate" with status "Pending" alongside the pre-loaded demo certificates. Click on it to see the full details: owner, team, environment, tags, and timeline.
Verify via API
curl -s $API/api/v1/certificates/mc-demo-api | jq '{id, name, common_name, status, environment, owner_id, team_id}'
Part 4: Trigger Certificate Renewal
In production, the scheduler automatically triggers renewal when certificates approach expiry. For this demo, we'll trigger it manually:
curl -s -X POST $API/api/v1/certificates/mc-demo-api/renew | jq .
Expected response:
{
"status": "renewal_triggered"
}
This creates a renewal job. Check the jobs list:
curl -s "$API/api/v1/jobs" | jq '.data[] | select(.certificate_id == "mc-demo-api") | {id, type, status, certificate_id}'
Check the dashboard. Go to the "Jobs" view — you'll see the renewal job for your certificate.
Part 5: Deploy the Certificate
Trigger deployment to see the deployment workflow:
curl -s -X POST $API/api/v1/certificates/mc-demo-api/deploy | jq .
Expected response:
{
"status": "deployment_triggered"
}
Check for deployment jobs:
curl -s "$API/api/v1/jobs" | jq '.data[] | select(.certificate_id == "mc-demo-api")'
Part 6: View the Audit Trail
Every action you've taken has been recorded. Check the audit trail:
curl -s $API/api/v1/audit | jq '.data[0:5]'
You'll see events for certificate creation, renewal trigger, and deployment trigger — each with actor, action, resource type, and timestamp.
Check the dashboard. The "Audit" view shows the full timeline of all actions across the system.
Part 7: Check Notifications
Certctl sends notifications for certificate lifecycle events. Check what notifications were generated:
curl -s $API/api/v1/notifications | jq '.data[0:5]'
In demo mode, notifications are marked as "sent" even without a real email/webhook backend. In production, these would go out via SMTP or HTTP webhooks.
Part 8: Create a Second Certificate and Compare
Let's create another certificate in production to see how the dashboard handles multiple environments:
curl -s -X POST $API/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"id": "mc-demo-payments",
"name": "Demo Payments Gateway",
"common_name": "payments.example.com",
"sans": ["payments.example.com", "checkout.example.com"],
"environment": "production",
"owner_id": "o-demo-user",
"team_id": "t-demo",
"issuer_id": "iss-local",
"renewal_policy_id": "rp-default",
"status": "Active",
"expires_at": "2026-04-01T00:00:00Z",
"tags": {
"service": "payments",
"pci": "true",
"tier": "critical"
}
}' | jq .
This certificate expires in about 18 days from the demo date, so it should show up as "Expiring" in the dashboard when the scheduler runs. Refresh the dashboard — you'll see it in the certificate list.
Now filter the dashboard by environment or status to see how the filtering works with your new certificates mixed in with the demo data.
Part 9: Policy Violations
Let's see what happens when a certificate doesn't meet policy requirements. Check existing policy rules:
curl -s $API/api/v1/policies | jq '.data[] | {id, name, type, enabled}'
The demo includes rules for required owner metadata, allowed environments, maximum certificate lifetime, and minimum renewal windows. Check existing violations:
curl -s "$API/api/v1/policies/pr-max-certificate-lifetime/violations" | jq .
In the dashboard, click "Policies" in the sidebar to see all active rules and which certificates are violating them.
Full Automated Script
Here's a single script that runs the entire demo end-to-end. Save it as demo.sh and run it:
#!/bin/bash
set -e
API="http://localhost:8443"
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${BLUE}=== certctl Advanced Demo ===${NC}"
echo ""
# Step 1: Health check
echo -e "${YELLOW}Step 1: Checking server health...${NC}"
HEALTH=$(curl -s $API/health | jq -r '.status')
if [ "$HEALTH" != "healthy" ]; then
echo "Server is not healthy. Run: docker compose -f deploy/docker-compose.yml up -d"
exit 1
fi
echo -e "${GREEN}Server is healthy${NC}"
echo ""
# Step 2: Create team
echo -e "${YELLOW}Step 2: Creating demo team...${NC}"
curl -s -X POST $API/api/v1/teams \
-H "Content-Type: application/json" \
-d '{"id":"t-demo-auto","name":"Automated Demo Team","description":"Created by demo script"}' | jq -r '.id'
echo -e "${GREEN}Team created${NC}"
echo ""
# Step 3: Create owner
echo -e "${YELLOW}Step 3: Registering demo owner...${NC}"
curl -s -X POST $API/api/v1/owners \
-H "Content-Type: application/json" \
-d '{"id":"o-demo-auto","name":"Demo Script","email":"demo-script@example.com","team_id":"t-demo-auto"}' | jq -r '.id'
echo -e "${GREEN}Owner registered${NC}"
echo ""
# Step 4: Create certificate
echo -e "${YELLOW}Step 4: Creating managed certificate...${NC}"
CERT_ID="mc-demo-$(date +%s)"
curl -s -X POST $API/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"id":"'$CERT_ID'",
"name":"Demo Auto Certificate",
"common_name":"auto-demo.internal.example.com",
"sans":["auto-demo.internal.example.com"],
"environment":"staging",
"owner_id":"o-demo-auto",
"team_id":"t-demo-auto",
"issuer_id":"iss-local",
"renewal_policy_id":"rp-default",
"status":"Pending",
"tags":{"created_by":"demo-script","automated":"true"}
}' | jq '{id, name, status}'
echo -e "${GREEN}Certificate created: $CERT_ID${NC}"
echo ""
# Step 5: Trigger renewal
echo -e "${YELLOW}Step 5: Triggering certificate renewal...${NC}"
curl -s -X POST $API/api/v1/certificates/$CERT_ID/renew | jq .
echo -e "${GREEN}Renewal triggered${NC}"
echo ""
# Step 6: Trigger deployment
echo -e "${YELLOW}Step 6: Triggering certificate deployment...${NC}"
curl -s -X POST $API/api/v1/certificates/$CERT_ID/deploy | jq .
echo -e "${GREEN}Deployment triggered${NC}"
echo ""
# Step 7: Check certificate status
echo -e "${YELLOW}Step 7: Checking certificate status...${NC}"
curl -s $API/api/v1/certificates/$CERT_ID | jq '{id, name, status, common_name, environment}'
echo ""
# Step 8: Check jobs
echo -e "${YELLOW}Step 8: Checking jobs...${NC}"
curl -s "$API/api/v1/jobs" | jq "[.data[] | select(.certificate_id == \"$CERT_ID\") | {id, type, status}]"
echo ""
# Step 9: View recent audit events
echo -e "${YELLOW}Step 9: Recent audit events...${NC}"
curl -s $API/api/v1/audit | jq '.data[0:3] | .[] | {action, resource_type, resource_id, timestamp}'
echo ""
# Step 10: Summary
echo -e "${BLUE}=== Demo Complete ===${NC}"
echo ""
echo "What happened:"
echo " 1. Created a team and owner for accountability"
echo " 2. Created a managed certificate tracked by certctl"
echo " 3. Triggered renewal (would contact the Local CA in production flow)"
echo " 4. Triggered deployment (would push to NGINX/F5/IIS targets)"
echo " 5. All actions recorded in the audit trail"
echo ""
echo -e "Open ${GREEN}http://localhost:8443${NC} to see everything in the dashboard."
echo "Look for certificate: $CERT_ID"
Make it executable and run:
chmod +x demo.sh
./demo.sh
What to Show Stakeholders
If you're using this demo to present certctl to decision-makers, here's the narrative:
- Start with the dashboard — "This is your certificate inventory. Every TLS certificate across your infrastructure, in one place."
- Point to expiring certs — "These certificates would have caused outages. Certctl catches them automatically."
- Show the cert you just created — "I just created this via the API. It's already tracked, assigned to a team, and will be renewed automatically."
- Show the audit trail — "Complete traceability. Every action, every change, every deployment — timestamped and attributed."
- Show policies — "Guardrails. We enforce that every certificate has an owner, uses approved CAs, and stays within allowed environments."
- Show agents — "Private keys never touch the control plane. Agents handle cryptographic operations locally on your infrastructure."
- Show the API — "Everything is API-first. The dashboard is just one consumer. You can integrate with CI/CD, Terraform, or custom tooling."
Teardown
docker compose -f deploy/docker-compose.yml down -v