Fix runtime bugs, implement service layer, and overhaul documentation

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>
This commit is contained in:
shankar0123
2026-03-14 21:38:11 -04:00
parent 3a9fe8ba37
commit 9b4122b159
21 changed files with 1597 additions and 1591 deletions
+148 -452
View File
@@ -1,36 +1,32 @@
# Certctl Quick Start Guide
# Quick Start Guide
Get a working certctl deployment from zero to managing certificates in 10 minutes.
Get certctl running locally and managing certificates in under 5 minutes.
New to certificates? Read the [Concepts Guide](concepts.md) first — it explains TLS, CAs, and private keys in plain language.
## Prerequisites
- **Docker** and **Docker Compose** (recommended), or:
- Go 1.22+
- PostgreSQL 14+
- psql CLI tool
You need **Docker** and **Docker Compose** installed. That's it.
## Option 1: Docker Compose (Fastest)
On macOS:
```bash
brew install --cask docker
```
### 1. Clone & Setup
On Linux, follow the official Docker install guide for your distribution.
## Start Everything
```bash
git clone https://github.com/shankar0123/certctl.git
cd certctl
# Copy environment template
cp .env.example .env
# Optional: edit .env for custom settings
# nano .env
docker compose -f deploy/docker-compose.yml up -d
```
### 2. Start the Stack
Wait about 30 seconds for PostgreSQL to initialize and the server to boot. Check that everything is healthy:
```bash
make docker-up
# Wait for services to be healthy (~30 seconds)
docker-compose -f deploy/docker-compose.yml ps
docker compose -f deploy/docker-compose.yml ps
```
You should see:
@@ -41,486 +37,186 @@ certctl-server Up (healthy)
certctl-agent Up
```
### 3. Verify Health
Verify the server responds:
```bash
# Server health check
curl http://localhost:8443/health
# Expected: {"status":"healthy"}
# Container logs
make docker-logs-server
```
---
## Option 2: Manual Build & Run
### 1. Clone & Dependencies
```bash
git clone https://github.com/shankar0123/certctl.git
cd certctl
go mod download
```
### 2. Setup PostgreSQL
```bash
# Create database and user
psql -U postgres -h localhost << EOF
CREATE USER certctl WITH PASSWORD 'certctl';
CREATE DATABASE certctl OWNER certctl;
GRANT ALL PRIVILEGES ON DATABASE certctl TO certctl;
EOF
# Verify connection
psql -h localhost -U certctl -d certctl -c "SELECT 1"
```
### 3. Run Migrations
```bash
# Install migrate tool
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Set database URL
export DB_URL="postgres://certctl:certctl@localhost:5432/certctl?sslmode=disable"
# Run migrations
make migrate-up
```
### 4. Start Server
```bash
# Terminal 1: Server
make run
# Expected output:
# 2024-03-14T10:30:00Z server starting version=1.0.0 server_port=8443
```
### 5. Start Agent (Optional)
```bash
# Terminal 2: Agent
export SERVER_URL=http://localhost:8443
export API_KEY=default-api-key
./bin/agent
# Expected output: Agent connecting to http://localhost:8443
```
---
## Walk-Through: Create Your First Certificate
### Step 1: Verify API Access
```bash
curl -X GET http://localhost:8443/health
```
Response:
```json
{"status":"healthy"}
```
### Step 2: Create a Team (Optional)
## Open the Dashboard
Teams organize ownership and auditing. For this quick start, we'll use a default team.
Open **http://localhost:8443** in your browser.
The dashboard comes pre-loaded with 14 demo certificates across multiple teams, environments, and statuses. You'll see expiring certs, expired certs, active certs, failed renewals — a realistic snapshot of what a certificate inventory looks like in a real organization.
Explore the sidebar: Certificates, Agents, Policies, Jobs, Audit Trail, Notifications. Everything you see in the dashboard is backed by the REST API.
## Explore the API
The dashboard reads from the same REST API you can call directly. All endpoints live under `/api/v1/` and return JSON.
### List all certificates
```bash
TEAM_ID="default"
curl -s http://localhost:8443/api/v1/certificates | jq .
```
### Step 3: Register an ACME Issuer
Create a certificate issuer configuration (Let's Encrypt staging for this demo):
```bash
curl -X POST http://localhost:8443/api/v1/issuers \
-H "Content-Type: application/json" \
-d '{
"team_id": "default",
"name": "lets-encrypt-staging",
"type": "acme",
"config": {
"directory_url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"email": "admin@example.com"
}
}'
```
Response (save the `issuer_id`):
The response has this shape:
```json
{
"id": "issuer-abc123",
"name": "lets-encrypt-staging",
"type": "acme",
"created_at": "2024-03-14T10:30:00Z"
}
```
Store the issuer ID:
```bash
ISSUER_ID="issuer-abc123"
```
### Step 4: Register an Agent
Agents handle certificate requests and deployment. Register one:
```bash
curl -X POST http://localhost:8443/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"team_id": "default",
"name": "quickstart-agent",
"description": "Local development agent"
}'
```
Response (save the `api_key` and `id`):
```json
{
"id": "agent-xyz789",
"name": "quickstart-agent",
"api_key": "ey...",
"registered_at": "2024-03-14T10:30:00Z",
"status": "registered"
}
```
Store the agent details:
```bash
AGENT_ID="agent-xyz789"
AGENT_API_KEY="ey..."
```
### Step 5: Create a Deployment Target
Targets are where certificates will be deployed (NGINX, F5, etc.). For this demo, we'll skip actual deployment:
```bash
curl -X POST http://localhost:8443/api/v1/targets \
-H "Content-Type: application/json" \
-d '{
"team_id": "default",
"agent_id": "'$AGENT_ID'",
"name": "example-nginx",
"type": "nginx",
"config": {
"host": "nginx.example.com",
"ssh_user": "deploy",
"ssh_key": "/path/to/key",
"cert_path": "/etc/nginx/ssl"
}
}'
```
Response:
```json
{
"id": "target-def456",
"name": "example-nginx",
"agent_id": "agent-xyz789",
"type": "nginx",
"status": "pending_validation"
}
```
Store the target ID:
```bash
TARGET_ID="target-def456"
```
### Step 6: Create a Managed Certificate
Now the main event—request a certificate to be issued and managed:
```bash
curl -X POST http://localhost:8443/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"team_id": "default",
"domain": "api.example.com",
"issuer_id": "'$ISSUER_ID'",
"target_ids": ["'$TARGET_ID'"],
"renewal_days_before": 30,
"auto_deploy": true
}'
```
Response:
```json
{
"id": "cert-ghi012",
"domain": "api.example.com",
"issuer_id": "issuer-abc123",
"status": "pending",
"created_at": "2024-03-14T10:30:00Z",
"expires_at": null,
"renewal_at": null
}
```
Store the certificate ID:
```bash
CERT_ID="cert-ghi012"
```
### Step 7: Check Certificate Status
Poll the certificate status as issuance progresses:
```bash
curl -X GET http://localhost:8443/api/v1/certificates/$CERT_ID \
-H "Content-Type: application/json"
```
Response (will change over time):
```json
{
"id": "cert-ghi012",
"domain": "api.example.com",
"status": "issued",
"expires_at": "2024-06-12T10:30:00Z",
"issued_by": "issuer-abc123",
"deployed_to": [
"data": [
{
"target_id": "target-def456",
"status": "success",
"deployed_at": "2024-03-14T10:30:30Z"
"id": "mc-api-prod",
"name": "API Production",
"common_name": "api.example.com",
"sans": ["api.example.com", "api-v2.example.com"],
"environment": "production",
"owner_id": "o-alice",
"team_id": "t-platform",
"issuer_id": "iss-local",
"status": "Active",
"expires_at": "2026-05-28T00:00:00Z",
"tags": {"service": "api-gateway", "tier": "critical"},
"created_at": "2026-03-14T00:00:00Z",
"updated_at": "2026-03-14T00:00:00Z"
}
]
],
"total": 14,
"page": 1,
"per_page": 50
}
```
### Step 8: View Audit Trail
See all actions related to your certificate:
### Filter by status
```bash
curl -X GET "http://localhost:8443/api/v1/audit/logs?resource_id=$CERT_ID" \
-H "Content-Type: application/json"
# Get only expiring certificates
curl -s "http://localhost:8443/api/v1/certificates?status=Expiring" | jq .
# Get only production certificates
curl -s "http://localhost:8443/api/v1/certificates?environment=production" | jq .
```
Response:
### Get a specific certificate
```bash
curl -s http://localhost:8443/api/v1/certificates/mc-api-prod | jq .
```
### List agents
```bash
curl -s http://localhost:8443/api/v1/agents | jq .
```
### View audit trail
```bash
curl -s http://localhost:8443/api/v1/audit | jq .
```
### View policy rules
```bash
curl -s http://localhost:8443/api/v1/policies | jq .
```
### View notifications
```bash
curl -s http://localhost:8443/api/v1/notifications | jq .
```
## Create Your First Certificate
Let's create a new managed certificate from scratch using the API. This will create a certificate record that certctl will track, renew, and deploy.
### Step 1: Create a certificate
```bash
curl -s -X POST http://localhost:8443/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"name": "My First Certificate",
"common_name": "myapp.example.com",
"sans": ["myapp.example.com", "www.myapp.example.com"],
"environment": "staging",
"owner_id": "o-alice",
"team_id": "t-platform",
"issuer_id": "iss-local",
"renewal_policy_id": "rp-default",
"status": "Pending",
"tags": {"purpose": "quickstart-demo"}
}' | jq .
```
The server returns the created certificate with an auto-generated ID:
```json
{
"logs": [
{
"id": "audit-001",
"timestamp": "2024-03-14T10:30:00Z",
"actor": {
"type": "api",
"id": "client-001"
},
"action": "certificate_created",
"resource": {
"type": "certificate",
"id": "cert-ghi012"
},
"status": "success"
},
{
"id": "audit-002",
"timestamp": "2024-03-14T10:30:10Z",
"actor": {
"type": "agent",
"id": "agent-xyz789"
},
"action": "certificate_issued",
"resource": {
"type": "certificate",
"id": "cert-ghi012"
},
"status": "success",
"details": {
"issuer": "lets-encrypt-staging",
"expiry": "2024-06-12"
}
},
{
"id": "audit-003",
"timestamp": "2024-03-14T10:30:25Z",
"actor": {
"type": "system",
"id": "scheduler"
},
"action": "certificate_deployed",
"resource": {
"type": "certificate",
"id": "cert-ghi012"
},
"status": "success",
"details": {
"deployed_to": "example-nginx"
}
}
]
"id": "a1b2c3d4-...",
"name": "My First Certificate",
"common_name": "myapp.example.com",
"status": "Pending",
"created_at": "2026-03-14T..."
}
```
### Step 9: Trigger Manual Renewal (Optional)
To manually trigger certificate renewal:
Save the certificate ID:
```bash
curl -X POST http://localhost:8443/api/v1/certificates/$CERT_ID/renew \
-H "Content-Type: application/json"
CERT_ID="<paste the id from the response>"
```
The scheduler will automatically check for renewals every hour. Certificates within 30 days of expiry are renewed automatically.
---
## Development Mode
For development with hot reload and database browser:
### Step 2: Trigger renewal
```bash
# Install tools
make install-tools
# Start dev stack (includes PgAdmin at localhost:5050)
make docker-up-dev
# View logs
make docker-logs-server
make docker-logs-agent
# Admin credentials for PgAdmin:
# Email: admin@example.com (default, see .env)
# Password: admin (default, see .env)
# Access PgAdmin: http://localhost:5050
# Add server: postgres, port 5432, user certctl, password certctl
curl -s -X POST http://localhost:8443/api/v1/certificates/$CERT_ID/renew | jq .
```
---
This creates a renewal job that will be processed by the scheduler.
## Testing the Flow End-to-End
Here's a complete script to test the full flow:
### Step 3: Check the certificate
```bash
#!/bin/bash
set -e
API="http://localhost:8443"
TEAM="default"
echo "1. Creating ACME issuer..."
ISSUER=$(curl -s -X POST $API/api/v1/issuers \
-H "Content-Type: application/json" \
-d '{
"team_id": "'$TEAM'",
"name": "letsencrypt-staging",
"type": "acme",
"config": {
"directory_url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"email": "test@example.com"
}
}' | jq -r '.id')
echo " Issuer: $ISSUER"
echo "2. Registering agent..."
AGENT=$(curl -s -X POST $API/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"team_id": "'$TEAM'",
"name": "test-agent"
}' | jq -r '.id')
echo " Agent: $AGENT"
echo "3. Creating certificate..."
CERT=$(curl -s -X POST $API/api/v1/certificates \
-H "Content-Type: application/json" \
-d '{
"team_id": "'$TEAM'",
"domain": "test-'$(date +%s)'.example.com",
"issuer_id": "'$ISSUER'",
"renewal_days_before": 30
}' | jq -r '.id')
echo " Certificate: $CERT"
echo "4. Checking status..."
curl -s -X GET $API/api/v1/certificates/$CERT | jq '.status'
echo "5. Viewing audit trail..."
curl -s -X GET "$API/api/v1/audit/logs?resource_id=$CERT" | jq '.logs | length'
echo "Done!"
curl -s http://localhost:8443/api/v1/certificates/$CERT_ID | jq .
```
Save as `test.sh`, make executable, and run:
```bash
chmod +x test.sh
./test.sh
```
---
## Common Issues
### Server Won't Start
### Step 4: Check the audit trail
```bash
# Check database connection
psql -h localhost -U certctl -d certctl -c "SELECT 1"
# View logs
make docker-logs-server
# Check environment
env | grep DB_
curl -s http://localhost:8443/api/v1/audit | jq '.data[0:3]'
```
### Agent Can't Connect
Refresh the dashboard at http://localhost:8443 — your new certificate appears in the inventory.
## Understanding the Demo Data
The demo comes pre-loaded with realistic data so you can explore certctl's features immediately:
| Resource | Count | Examples |
|----------|-------|---------|
| Teams | 5 | Platform, Security, Payments, Frontend, Data |
| Owners | 5 | Alice, Bob, Carol, Dave, Eve |
| Issuers | 3 | Local Dev CA, Let's Encrypt Staging, DigiCert |
| Agents | 5 | nginx-prod, nginx-staging, f5-prod, iis-prod, data-agent |
| Targets | 5 | NGINX (prod/staging/data), F5 LB, IIS |
| Certificates | 14 | Various statuses: Active, Expiring, Expired, Failed |
| Policies | 4 | Required owner, allowed environments, max lifetime, min renewal window |
Certificates have varied statuses so you can see what each state looks like in the dashboard: healthy certs with 45+ days remaining, certs about to expire (5-12 days), certs that already expired, and a failed renewal.
## Tear Down
```bash
# Verify server is running
curl http://localhost:8443/health
# Check agent logs
docker logs certctl-agent
# Verify API key is correct
echo $AGENT_API_KEY
docker compose -f deploy/docker-compose.yml down -v
```
### Certificate Stays "Pending"
The `-v` flag removes the PostgreSQL data volume so you get a clean slate next time.
```bash
# Check if agent is registered
curl http://localhost:8443/api/v1/agents
## What's Next
# Check agent logs for errors
make docker-logs-agent
# View certificate details
curl http://localhost:8443/api/v1/certificates/$CERT_ID
# Check audit trail
curl "http://localhost:8443/api/v1/audit/logs?resource_id=$CERT_ID"
```
---
## Next Steps
1. **Read** [docs/architecture.md](architecture.md) to understand the design
2. **Explore** the [API](../README.md#api-overview) for more operations
3. **Build** a [custom connector](connectors.md) for your infrastructure
4. **Deploy** to production using [docs/k8s-deployment.md](k8s-deployment.md) (coming soon)
---
For more help, see [README.md](../README.md#troubleshooting) or open an issue on GitHub.
- **[Advanced Demo](demo-advanced.md)** — Issue a real certificate via the Local CA and watch it appear in the dashboard
- **[Demo Walkthrough](demo-guide.md)** — Guided 5-minute stakeholder presentation
- **[Architecture](architecture.md)** — How the control plane, agents, and connectors work together
- **[Connector Guide](connectors.md)** — Build custom connectors for your infrastructure