mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:51:30 +00:00
9b4122b159
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>
369 lines
11 KiB
Markdown
369 lines
11 KiB
Markdown
# Connector Development Guide
|
|
|
|
Connectors extend certctl to integrate with external systems for certificate issuance, deployment, and notifications. This guide covers the connector interfaces, built-in implementations, and how to build your own.
|
|
|
|
## Overview
|
|
|
|
Three types of connectors:
|
|
|
|
1. **Issuer Connector** — Obtains certificates from CAs (ACME, Local CA, Vault, DigiCert)
|
|
2. **Target Connector** — Deploys certificates to infrastructure (NGINX, F5, IIS)
|
|
3. **Notifier Connector** — Sends alerts about certificate events (Email, Webhooks, Slack)
|
|
|
|
All connectors accept JSON configuration at initialization, support config validation, and are registered in the service layer. Issuer connectors run on the control plane; target connectors run on agents.
|
|
|
|
## Issuer Connector
|
|
|
|
Issuer connectors obtain signed certificates from Certificate Authorities.
|
|
|
|
### Interface
|
|
|
|
```go
|
|
// internal/connector/issuer/interface.go
|
|
package issuer
|
|
|
|
type Connector interface {
|
|
// ValidateConfig checks that the issuer configuration is valid
|
|
ValidateConfig(ctx context.Context, config json.RawMessage) error
|
|
|
|
// IssueCertificate submits a CSR and returns a signed certificate
|
|
IssueCertificate(ctx context.Context, request IssuanceRequest) (*IssuanceResult, error)
|
|
|
|
// RenewCertificate renews an existing certificate
|
|
RenewCertificate(ctx context.Context, request RenewalRequest) (*IssuanceResult, error)
|
|
|
|
// RevokeCertificate revokes a previously issued certificate
|
|
RevokeCertificate(ctx context.Context, request RevocationRequest) error
|
|
|
|
// GetOrderStatus checks the status of an async issuance order
|
|
GetOrderStatus(ctx context.Context, orderID string) (*OrderStatus, error)
|
|
}
|
|
|
|
type IssuanceRequest struct {
|
|
CommonName string
|
|
SANs []string
|
|
CSRPEM string
|
|
}
|
|
|
|
type IssuanceResult struct {
|
|
CertPEM string
|
|
ChainPEM string
|
|
Serial string
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
OrderID string
|
|
}
|
|
|
|
type RenewalRequest struct {
|
|
CommonName string
|
|
SANs []string
|
|
CSRPEM string
|
|
OrderID string // optional, for tracking
|
|
}
|
|
|
|
type RevocationRequest struct {
|
|
Serial string
|
|
Reason string // optional
|
|
}
|
|
|
|
type OrderStatus struct {
|
|
OrderID string
|
|
Status string // "pending", "valid", "invalid", "expired"
|
|
Message string
|
|
CertPEM string
|
|
ChainPEM string
|
|
Serial string
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
```
|
|
|
|
### Built-in: Local CA
|
|
|
|
The Local CA issuer generates self-signed certificates using Go's `crypto/x509` library. It creates a CA on first use (in memory), issues certificates with proper serial numbers, validity periods, SANs, and key usage extensions.
|
|
|
|
This issuer is designed for development and demos only — certificates are self-signed and not trusted by browsers.
|
|
|
|
Configuration:
|
|
```json
|
|
{
|
|
"ca_common_name": "CertCtl Local CA",
|
|
"validity_days": 90
|
|
}
|
|
```
|
|
|
|
Location: `internal/connector/issuer/local/local.go`
|
|
|
|
### Building a Custom Issuer
|
|
|
|
Here's the structure for a HashiCorp Vault PKI issuer:
|
|
|
|
```go
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
vaultapi "github.com/hashicorp/vault/api"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer"
|
|
)
|
|
|
|
type Config struct {
|
|
Address string `json:"address"`
|
|
Token string `json:"token"`
|
|
PKIPath string `json:"pki_path"`
|
|
RoleName string `json:"role_name"`
|
|
}
|
|
|
|
type VaultIssuer struct {
|
|
config *Config
|
|
client *vaultapi.Client
|
|
}
|
|
|
|
func New(cfg *Config) (*VaultIssuer, error) {
|
|
client, err := vaultapi.NewClient(&vaultapi.Config{Address: cfg.Address})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vault client: %w", err)
|
|
}
|
|
client.SetToken(cfg.Token)
|
|
return &VaultIssuer{config: cfg, client: client}, nil
|
|
}
|
|
|
|
func (v *VaultIssuer) ValidateConfig(ctx context.Context, config json.RawMessage) error {
|
|
var cfg Config
|
|
if err := json.Unmarshal(config, &cfg); err != nil {
|
|
return fmt.Errorf("invalid config: %w", err)
|
|
}
|
|
if cfg.Address == "" || cfg.Token == "" {
|
|
return fmt.Errorf("address and token are required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *VaultIssuer) IssueCertificate(ctx context.Context, req issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
|
|
path := fmt.Sprintf("%s/sign/%s", v.config.PKIPath, v.config.RoleName)
|
|
secret, err := v.client.Logical().Write(path, map[string]interface{}{
|
|
"common_name": req.CommonName,
|
|
"alt_names": req.SANs,
|
|
"csr": req.CSRPEM,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vault sign: %w", err)
|
|
}
|
|
|
|
return &issuer.IssuanceResult{
|
|
CertPEM: secret.Data["certificate"].(string),
|
|
ChainPEM: secret.Data["ca_chain"].(string),
|
|
Serial: secret.Data["serial_number"].(string),
|
|
}, nil
|
|
}
|
|
|
|
// ... implement RenewCertificate, RevokeCertificate, GetOrderStatus
|
|
```
|
|
|
|
## Target Connector
|
|
|
|
Target connectors deploy certificates to infrastructure systems. They run on agents, not on the control plane.
|
|
|
|
### Interface
|
|
|
|
```go
|
|
// internal/connector/target/interface.go
|
|
package target
|
|
|
|
type Connector interface {
|
|
// ValidateConfig checks target configuration
|
|
ValidateConfig(ctx context.Context, config json.RawMessage) error
|
|
|
|
// DeployCertificate pushes a certificate to the target system
|
|
DeployCertificate(ctx context.Context, request DeploymentRequest) (*DeploymentResult, error)
|
|
|
|
// ValidateDeployment verifies a certificate was deployed correctly
|
|
ValidateDeployment(ctx context.Context, request ValidationRequest) (*ValidationResult, error)
|
|
}
|
|
|
|
type DeploymentRequest struct {
|
|
CertPEM string // Signed certificate (PEM)
|
|
ChainPEM string // CA chain (PEM)
|
|
TargetConfig json.RawMessage // Target-specific config
|
|
Metadata map[string]string
|
|
// NOTE: No private key — agents handle keys locally
|
|
}
|
|
|
|
type DeploymentResult struct {
|
|
Success bool
|
|
TargetAddress string
|
|
DeploymentID string
|
|
Message string
|
|
DeployedAt time.Time
|
|
Metadata map[string]string
|
|
}
|
|
|
|
type ValidationRequest struct {
|
|
CertificateID string
|
|
Serial string
|
|
TargetConfig json.RawMessage
|
|
Metadata map[string]string
|
|
}
|
|
|
|
type ValidationResult struct {
|
|
Valid bool
|
|
Serial string
|
|
TargetAddress string
|
|
Message string
|
|
ValidatedAt time.Time
|
|
Metadata map[string]string
|
|
}
|
|
```
|
|
|
|
### Built-in: NGINX
|
|
|
|
The NGINX connector writes certificate and chain files to disk, validates the NGINX configuration, and reloads the server.
|
|
|
|
Configuration:
|
|
```json
|
|
{
|
|
"cert_path": "/etc/nginx/certs/cert.pem",
|
|
"chain_path": "/etc/nginx/certs/chain.pem",
|
|
"reload_command": "systemctl reload nginx",
|
|
"validate_command": "nginx -t"
|
|
}
|
|
```
|
|
|
|
The connector writes cert and chain files with mode 0644, runs the validation command first (so a bad config doesn't take down NGINX), and only reloads if validation passes.
|
|
|
|
Location: `internal/connector/target/nginx/nginx.go`
|
|
|
|
### Built-in: F5 BIG-IP
|
|
|
|
Deploys certificates via the F5 REST API. Uploads the certificate and key, then updates virtual server SSL profiles.
|
|
|
|
Location: `internal/connector/target/f5/f5.go`
|
|
|
|
### Built-in: IIS
|
|
|
|
Deploys certificates to Microsoft IIS via WinRM. Imports the certificate into the Windows certificate store and binds it to an IIS site.
|
|
|
|
Location: `internal/connector/target/iis/iis.go`
|
|
|
|
## Notifier Connector
|
|
|
|
Notifier connectors send alerts about certificate lifecycle events (expiration warnings, renewal success/failure, deployment status, policy violations).
|
|
|
|
### Interface
|
|
|
|
The service layer defines a simple notifier interface:
|
|
|
|
```go
|
|
// internal/service/notification.go
|
|
|
|
type Notifier interface {
|
|
Send(ctx context.Context, recipient string, subject string, body string) error
|
|
Channel() string
|
|
}
|
|
```
|
|
|
|
The connector layer has a richer interface:
|
|
|
|
```go
|
|
// internal/connector/notifier/interface.go
|
|
|
|
type Connector interface {
|
|
ValidateConfig(ctx context.Context, config json.RawMessage) error
|
|
SendAlert(ctx context.Context, alert Alert) error
|
|
SendEvent(ctx context.Context, event Event) error
|
|
}
|
|
```
|
|
|
|
Built-in notifiers: **Email** (SMTP) and **Webhook** (HTTP POST).
|
|
|
|
In demo mode, notifications are marked as "sent" even without a configured notifier — this prevents error spam in the logs while still generating notification records for the dashboard to display.
|
|
|
|
## Registering a Connector
|
|
|
|
To add a new connector:
|
|
|
|
1. Create a package under the appropriate directory:
|
|
- `internal/connector/issuer/myissuer/`
|
|
- `internal/connector/target/mytarget/`
|
|
- `internal/connector/notifier/mynotifier/`
|
|
|
|
2. Implement the interface (all methods required)
|
|
|
|
3. Register it in the service layer during server initialization in `cmd/server/main.go`:
|
|
|
|
```go
|
|
// For issuers
|
|
issuerRegistry := map[string]service.IssuerConnector{
|
|
"local": localCAConnector,
|
|
"acme": acmeConnector,
|
|
"vault": vaultConnector, // your new issuer
|
|
}
|
|
|
|
// For notifiers
|
|
notifierRegistry := map[string]service.Notifier{
|
|
"Email": emailNotifier,
|
|
"Webhook": webhookNotifier,
|
|
"Slack": slackNotifier, // your new notifier
|
|
}
|
|
```
|
|
|
|
## Testing Connectors
|
|
|
|
### Unit Tests
|
|
|
|
```go
|
|
func TestNginxDeploy(t *testing.T) {
|
|
cfg := &nginx.Config{
|
|
CertPath: "/tmp/test-cert.pem",
|
|
ChainPath: "/tmp/test-chain.pem",
|
|
ReloadCommand: "echo reloaded",
|
|
ValidateCommand: "echo valid",
|
|
}
|
|
connector := nginx.New(cfg, slog.Default())
|
|
|
|
result, err := connector.DeployCertificate(ctx, target.DeploymentRequest{
|
|
CertPEM: testCertPEM,
|
|
ChainPEM: testChainPEM,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("deploy failed: %v", err)
|
|
}
|
|
if !result.Success {
|
|
t.Fatal("expected success")
|
|
}
|
|
}
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```bash
|
|
# Start dependent service
|
|
docker run -d --name nginx -p 8080:80 nginx:latest
|
|
|
|
# Run tests
|
|
go test -tags=integration ./internal/connector/target/nginx/
|
|
|
|
# Cleanup
|
|
docker rm -f nginx
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Always validate config** — Check all required fields in `ValidateConfig` before any operation
|
|
2. **Use context for timeouts** — All connector methods accept `context.Context`; honor cancellation and deadlines
|
|
3. **Return descriptive errors** — Wrap errors with context so failures are diagnosable from logs
|
|
4. **Never log secrets** — Don't log API tokens, passwords, or private key material
|
|
5. **Support dry-run** — Where possible, support a validation/dry-run mode for deployment testing
|
|
6. **Idempotent operations** — Deploying the same certificate twice should succeed, not fail
|
|
7. **Report metadata** — Return deployment duration, target address, and other useful data in results
|
|
|
|
## What's Next
|
|
|
|
- [Architecture Guide](architecture.md) — Understanding the full system design
|
|
- [Quick Start](quickstart.md) — Get certctl running locally
|
|
- [Advanced Demo](demo-advanced.md) — See the full certificate lifecycle in action
|