Certificate Validate

A modern, extensible SSL/TLS certificate validation tool written in Go. Fetches and inspects certificate information from remote hosts via CLI or HTTP API.
For AI agents: this README is the human-facing usage/feature reference. Project
working conventions (English, SOLID, coverage, lefthook, invariants) live in
AGENTS.md, which is the canonical instruction file for AI coding agents.
Features
- CLI Mode — Check certificates with output formats (JSON/table), watch mode, filtering
- HTTP API — RESTful API with Swagger UI, rate limiting, optional API key auth
- Export — Export certificate data to JSON or CSV via CLI or API
- Watch Mode — Continuous checking with configurable interval
- Alert Webhook — POST alerts when certificates approach expiration
- History — Records check results in JSONL with automatic rotation
- Prometheus Metrics — Exposes
certificate_days_left and certificate_expired gauges
- Revocation Checks — OCSP + CRL validation per certificate
- Kubernetes monitoring —
k8s monitor scans TLS Secrets + Ingresses, exports Prometheus metrics with K8s labels, fires webhook alerts, and can auto-renew expiring certs via cert-manager (read-only by default, opt-in renewal; see docs/K8S_INTEGRATION.md)
- Hot-Reload —
SIGHUP reloads config without restarting the server
- Self-Signed CAs — Global and per-host trusted CA certificates
- Environment Variables —
CV_ prefix overrides for all config fields
- Concurrent Processing — Parallel certificate fetching with semaphore
- Minimal Dependencies — Small Docker image (~10MB)
- Extensible Architecture — SOLID principles with clean interfaces
Problems It Solves
| Problem |
What certificate-validate does |
| A certificate expires and takes a service down |
Continuously checks days-left and alerts before the deadline |
| A revoked certificate still looks valid |
Verifies OCSP first and falls back to CRL distribution points |
| TLS Secrets and Ingresses sprawl across Kubernetes |
Discovers every TLS resource, labels metrics by namespace/name/kind, and can renew via cert-manager |
| Internal CAs and self-signed chains fail silently |
Validates chains against global or per-host CA bundles |
| Nobody sees certificate health until an incident |
Exposes Prometheus metrics, a dashboard, JSONL history, and webhook alerts |
| Renewals and audits depend on manual work |
Runs as a CLI, API, or cluster agent and exports JSON/CSV evidence |
Quick Start
# From source
go install github.com/fabianoflorentino/certificate-validate/cmd/certificate-validate@latest
# Check one host without a config file
certificate-validate check --host github.com -o table
# Build locally
git clone https://github.com/fabianoflorentino/certificate-validate.git
cd certificate-validate
go build -o certificate-validate ./cmd/certificate-validate
# Docker
docker build -t certificate-validate .
# Run CLI check against config/settings.yml
./certificate-validate check
# Run API server + dashboard
./certificate-validate serve
Want a reproducible end-to-end example? Run make demo/test or see
demo/README.md.
Documentation
Architecture
flowchart LR
subgraph Interfaces
CLI["CLI\ncheck | export | serve"]
API["HTTP API\n/api/v1/cert/*"]
K8S["k8s monitor"]
end
CLI --> CHECKER["checker\norchestration"]
API --> SERVICE["service\nfacade"]
SERVICE --> CHECKER
K8S --> K8SMON["k8smonitor\ndiscovery + analysis"]
CHECKER --> FETCHER["fetcher\nTLS dial"]
CHECKER --> FORMATTER["formatter\nJSON | table | CSV"]
FETCHER --> CERT["certificate\ndomain model"]
FETCHER --> REVOC["revocation\nOCSP + CRL"]
K8SMON --> K8SAPI[("Kubernetes API\nSecrets + Ingresses")]
K8SMON --> K8SMETRICS["Prometheus metrics"]
K8SMON --> WEBHOOK["Webhook alerts"]
K8SMON --> RENEWER["cert-manager\nrenewal (opt-in)"]
SERVICE --> HISTORY["history\nJSONL"]
SERVICE --> METRICS["metrics\nPrometheus"]
For the detailed request flows, sequence diagrams, and dependency-injection
pipeline, see docs/ARCHITECTURE.md.
certificate-validate/
├── cmd/certificate-validate/
│ └── main.go # Entry point
├── config/
│ └── settings.yml # YAML configuration
├── docs/
│ ├── ARCHITECTURE.md # Architecture & flowcharts
│ └── swagger.yaml # OpenAPI 3.0 spec
├── internal/
│ ├── api/ # HTTP handlers + middleware + rate limiter
│ │ ├── api.go # Routes, handlers, auth, rate limiting
│ │ └── static/ # Embedded frontend + Swagger UI
│ ├── certificate/ # Domain: Certificate value object
│ │ ├── certificate.go # FromX509, TLSVersionString, BuildChain
│ │ └── errors.go # Domain-specific errors
│ ├── checker/ # Use case: orchestration
│ │ └── checker.go # Checker (Fetcher + Formatter)
│ ├── cmd/ # CLI (Cobra)
│ │ ├── root.go # Root + completion + global flags
│ │ ├── check.go # Check certificates
│ │ ├── serve.go # HTTP API server
│ │ ├── export.go # Export JSON/CSV
│ │ ├── k8s.go # `k8s monitor` (Kubernetes agent)
│ │ └── version.go # Version info
│ ├── config/ # YAML loader + env var overrides
│ │ └── config.go # Load, Validate, applyEnvOverrides
│ ├── fetcher/ # TLS fetcher with CA support
│ │ └── fetcher.go # Fetcher + per-host CAs
│ ├── formatter/ # Output formatters
│ │ └── formatter.go # FormatTable, FormatJSON, FormatCSV
│ ├── history/ # JSONL history recorder
│ │ └── history.go # Record, rotate, query
│ ├── k8smonitor/ # Kubernetes certificate monitoring (Phases 1–2)
│ │ ├── client.go # client-go setup + discovery (Secrets/Ingresses)
│ │ ├── analyzer.go # Bundle parse + chain/OCSP/CRL analysis
│ │ ├── monitor.go # Scan orchestration (single scan + watch)
│ │ ├── renewer.go # Auto-renewal (annotate → wait → validate)
│ │ ├── model.go # K8sCertificate model
│ │ ├── metrics.go # Dedicated Prometheus registry + gauges
│ │ └── webhook.go # Rate-limited alert webhook
│ ├── metrics/ # Prometheus exposition
│ │ └── metrics.go # Gauges + /metrics handler
│ ├── notifier/ # Webhook alerts
│ │ └── notifier.go # Periodic alert checks
│ ├── revocation/ # OCSP + CRL checks
│ │ └── revocation.go # CheckOCSP, CheckCRL
│ └── service/ # Facade layer
│ └── service.go # CertService
├── kubernetes/
│ └── monitor/ # K8s monitor deployment (rbac, daemonset, service, servicemonitor, prometheusrule)
├── scripts/ # Dev/demo scripts (setup-cluster, teardown, monitor-logs, alert-demo, webhook-receiver)
├── demo/
│ ├── compose/ # Deterministic TLS/API/Prometheus/webhook demo
│ ├── kind/ # kind + cert-manager end-to-end demo wrapper
│ └── README.md # Demo guide (self-signed and real certificates)
├── dev/
│ └── README.md # Disposable Kubernetes dev environment guide
├── Dockerfile # Multi-stage: `production` (default) + `dev`
├── docker-compose.yml # Services: certificate-validate (app) + dev (profile)
├── Makefile
└── README.md
Configuration
Create a config/settings.yml file:
check_time: 30 # Interval for watch/history/metrics (seconds)
api_key: "sk-1234" # API key for X-API-Key auth (optional)
app_configs:
- name: 'certificate-validate'
host: '0.0.0.0'
port: '5000'
environment: 'production'
debug: false
hosts:
- name: "GitHub"
url: 'github.com'
port: '443'
# ports: [443, 8443] # Multiple ports per host (optional)
# timeout: 10 # Per-host dial timeout (seconds)
# trusted_cas: # Per-host trusted CAs
# - '/certs/internal-ca.pem'
prometheus:
enabled: false # Enable Prometheus metrics at /metrics
# address: ':9090'
webhook:
# url: 'https://hooks.example.com/alert'
threshold: 15 # Alert when days left drops below this
interval: 1800 # Check interval (seconds)
history:
enabled: true # Record check history to JSONL
file_path: "data/history.jsonl"
max_entries: 10000 # Rotate after this many entries
max_days: 90 # Remove entries older than this
trusted_cas:
# - '/etc/certificates/my-ca.pem'
Environment Variables
All config fields can be overridden via environment variables with the CV_ prefix:
| Variable |
Overrides |
CV_CHECK_TIME |
check_time |
CV_API_KEY |
api_key |
CV_APP_HOST |
app_configs[0].host |
CV_APP_PORT |
app_configs[0].port |
CV_PROMETHEUS_ENABLED |
prometheus.enabled |
CV_PROMETHEUS_ADDRESS |
prometheus.address |
CV_WEBHOOK_URL |
webhook.url |
CV_WEBHOOK_THRESHOLD |
webhook.threshold |
CV_WEBHOOK_INTERVAL |
webhook.interval |
CV_HISTORY_ENABLED |
history.enabled |
CV_HISTORY_FILE_PATH |
history.file_path |
CV_HISTORY_MAX_ENTRIES |
history.max_entries |
CV_HISTORY_MAX_DAYS |
history.max_days |
CV_TRUSTED_CAS |
trusted_cas (comma-separated) |
Example:
CV_API_KEY="sk-secret" CV_WEBHOOK_URL="https://hooks.example.com/alert" ./certificate-validate serve
CLI Usage
Global Flags
| Flag |
Description |
-c, --config |
Path to config file (default: config/settings.yml) |
--log-file |
Write structured logs to file (also written to stderr) |
certificate-validate check
Check certificates from configured hosts or a single host.
./certificate-validate check # All hosts from config
./certificate-validate check -o table # Table output
./certificate-validate check --host github.com # Single host, no config
./certificate-validate check --host github.com --port 8443
./certificate-validate check --min-days 30 # Only show certs ≤30 days
./certificate-validate check --watch # Continuous checking
| Flag |
Description |
-w, --watch |
Continuously check at the configured interval |
-o, --output |
Output format: json (default) or table |
--host |
Check a single host directly (no config needed) |
--port |
Port for --host (default: 443) |
--min-days |
Only show certificates with ≤ this many days remaining |
certificate-validate serve
Start the HTTP API server.
./certificate-validate serve # HTTP on :5000
./certificate-validate serve --tls-cert cert.pem --tls-key key.pem # HTTPS
./certificate-validate serve --api-key "sk-1234" # Require API key
| Flag |
Description |
--tls-cert |
Path to TLS certificate file |
--tls-key |
Path to TLS private key file |
--api-key |
API key (overrides config/api_key) |
Send SIGHUP to reload configuration without restarting:
kill -HUP <pid>
certificate-validate export
Export certificate data to JSON or CSV.
./certificate-validate export # JSON to stdout
./certificate-validate export -f csv # CSV to stdout
./certificate-validate export -f json -o data/certs.json # Write to file
| Flag |
Description |
-f, --format |
Output format: json (default) or csv |
-o, --output-file |
Write to file instead of stdout |
certificate-validate k8s monitor
Monitor TLS certificates in a Kubernetes cluster (Phases 1–2 of the Kubernetes
integration). Discovers TLS Secrets and Ingresses, analyzes
each certificate, exports Prometheus metrics, and optionally fires webhook alerts.
By default it is read-only (no cluster writes); set --renew-threshold to enable
Phase 2 auto-renewal of expiring certificates via cert-manager.
# Single scan, print each certificate as JSON
./certificate-validate k8s monitor
# Watch mode: scan every 5 minutes, serve metrics on :9102, alert on webhook
./certificate-validate k8s monitor \
--watch-interval=300 \
--metrics-addr=:9102 \
--webhook-url https://hooks.example.com/alert \
--check-revocation
# Restrict to specific namespaces
./certificate-validate k8s monitor -n cert-manager -n default
# Auto-renew certificates at or below 15 days left (via cert-manager)
./certificate-validate k8s monitor --watch-interval=300 --renew-threshold=15
| Flag |
Default |
Description |
-n, --namespace |
all |
Namespaces to scan (repeatable / comma-separated) |
--kubeconfig |
in-cluster / ~/.kube/config |
Path to kubeconfig |
--check-revocation |
false |
Perform OCSP/CRL revocation checks |
--watch-interval |
0 (single scan) |
Repeat scan every N seconds |
--webhook-url |
empty |
URL to POST expiring-cert alerts |
--webhook-threshold |
15 |
Alert when days-left is at/below this value |
--webhook-interval |
300 |
Min seconds between alerts per resource |
--renew-threshold |
0 (disabled) |
Auto-renew certs at/below this many days left |
--renew-timeout |
120 |
Max seconds to wait for cert-manager to re-issue |
--metrics-addr |
empty |
Serve Prometheus metrics on this address (e.g. :9102) |
Deployment manifests (DaemonSet, RBAC, Service, ServiceMonitor) live in
kubernetes/monitor/. Test locally with the disposable dev
environment (make dev/up … make dev/setup, see dev/README.md).
certificate-validate version
Print build information.
$ ./certificate-validate version
certificate-validate v1.0.0
commit: abc1234
built: 2026-07-02T00:00:00Z
go: go1.24 linux/amd64
certificate-validate completion
Generate shell completion scripts.
source <(./certificate-validate completion bash)
source <(./certificate-validate completion zsh)
./certificate-validate completion fish | source
API
The API server is available at http://localhost:5000. Interactive docs at http://localhost:5000/swagger/.
API Key Authentication
When api_key is configured (via config file, --api-key flag, or CV_API_KEY env var), all routes except /health require the X-API-Key header:
curl -H "X-API-Key: sk-1234" http://localhost:5000/api/v1/cert/info/all
Rate Limiting
All requests (except static files) are rate-limited via a token bucket: 100 req/s, burst 200. Exceeded requests receive 429 Too Many Requests with a Retry-After: 1 header.
Endpoints
| Method |
Route |
Description |
| GET |
/ |
Dashboard (embedded frontend) |
| GET |
/swagger/ |
Swagger UI (interactive API docs) |
| GET |
/swagger.yaml |
OpenAPI 3.0 specification |
| GET |
/health |
Health check (pings all hosts) |
| GET |
/api/v1/cert/info/all |
All certificates from configured hosts |
| GET |
/api/v1/cert/info/{hostname} |
Certificate for a specific host |
| GET |
/api/v1/cert/info/commonName |
Map of hostname → Common Name |
| GET |
/api/v1/cert/info/subjectAltName |
Map of hostname → SANs |
| GET |
/api/v1/cert/export/json |
Download all certificates as JSON file |
| GET |
/api/v1/cert/export/csv |
Download all certificates as CSV file |
| GET |
/api/v1/cert/history/{hostname} |
History of checks for a host |
| GET |
/metrics |
Prometheus metrics (if prometheus.enabled) |
Example Response
{
"commonName": "github.com",
"subjectAltName": ["github.com", "www.github.com"],
"issuer": "Sectigo Public Server Authentication CA DV E36",
"type": "Domain Validation (DV) Web Server SSL Digital Certificate",
"notBefore": "2024-01-01 00:00:00",
"notAfter": "2025-01-01 23:59:59",
"daysLeft": 365,
"crl": ["http://crl.example.com/ca.crl"],
"ocsp": ["http://ocsp.example.com"],
"revocationStatus": "unknown",
"hostname": "github.com",
"port": 443,
"tlsVersion": "TLS 1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"chain": [
{
"subject": "CN=github.com,O=GitHub\\, Inc.,L=San Francisco,ST=California,C=US",
"issuer": "CN=Sectigo Public Server Authentication CA DV E36",
"notAfter": "2025-01-01 23:59:59",
"fingerprint": "a1b2c3d4e5f6..."
}
]
}
Docker
CLI Mode
docker run -v $(pwd)/config:/app/config certificate-validate check
docker run -v $(pwd)/config:/app/config certificate-validate check --watch
API Mode
docker run -p 5000:5000 -v $(pwd)/config:/app/config certificate-validate serve
Docker Compose
docker-compose up -d
curl http://localhost:5000/api/v1/cert/info/all
curl http://localhost:5000/swagger/
Observability
Metrics
With prometheus.enabled: true, the API server exposes metrics at /metrics
(labels host, port):
| Metric |
Type |
Meaning |
certificate_days_left |
Gauge |
Days remaining before expiration |
certificate_expired |
Gauge |
1 if expired, 0 if valid |
k8s monitor --metrics-addr=:9102 exposes the same metric names on a dedicated
registry, labelled by Kubernetes resource (namespace, name, kind), plus
certificate_revoked and the Phase 2 renewal metrics. See
docs/K8S_INTEGRATION.md.
Example /metrics output from the core server:
# HELP certificate_days_left Days remaining before certificate expiration
# TYPE certificate_days_left gauge
certificate_days_left{host="github.com",port="443"} 87
# HELP certificate_expired Whether the certificate has expired (1=expired, 0=valid)
# TYPE certificate_expired gauge
certificate_expired{host="github.com",port="443"} 0
Example /metrics output from k8s monitor:
# HELP certificate_days_left Days remaining before certificate expiration
# TYPE certificate_days_left gauge
certificate_days_left{kind="Secret",name="example-tls-secret",namespace="default"} 89
# HELP certificate_expired Whether the certificate has expired (1=expired, 0=valid)
# TYPE certificate_expired gauge
certificate_expired{kind="Secret",name="example-tls-secret",namespace="default"} 0
Useful PromQL queries:
# Certificates expiring within 15 days
certificate_days_left < 15
# Expired certificates
certificate_expired == 1
# Revoked Kubernetes certificates
certificate_revoked == 1
# Lowest days-left per Kubernetes namespace
min by (namespace) (certificate_days_left)
A ready-to-apply PrometheusRule with these alerts lives in
kubernetes/monitor/prometheusrule.yml.
Alert webhook payload
When webhook.url is set (core) or --webhook-url is passed (k8s monitor),
certificates at or below the threshold produce a JSON POST. Core payload:
{
"hostname": "github.com",
"port": 443,
"commonName": "github.com",
"issuer": "Sectigo Public Server Authentication CA DV E36",
"daysLeft": 12,
"threshold": 15,
"message": "Certificate for github.com expires in 12 days"
}
Kubernetes payload:
{
"namespace": "default",
"name": "example-tls-secret",
"kind": "Secret",
"hostname": "example.com",
"commonName": "example.com",
"issuer": "cert-manager selfsigned CA",
"daysLeft": 12,
"threshold": 15,
"message": "Certificate default/example-tls-secret (Secret) expires in 12 days"
}
Alerts are rate-limited per certificate so the same resource is not re-alerted
on every scan.
End-to-end demo
The disposable Kubernetes environment includes a runnable monitoring and
alerting scenario (kind + cert-manager + webhook receiver):
make dev/up # build and start the dev container
make dev/setup # kind cluster + cert-manager + monitor manifests
make dev/alert-demo # metrics + webhook alert demo
make dev/alert-demo creates a 90-day certificate, runs k8s monitor in watch
mode with --metrics-addr and --webhook-url, prints the certificate_*
metrics, and prints the alert payload received by the local webhook receiver.
See dev/README.md.
Deterministic Demos
Two self-contained demos use only self-signed certificates and local
Docker DNS — no Let's Encrypt, public DNS, or external CA required.
| Demo |
Command |
What it proves |
| Compose |
make demo/test |
CLI, API, metrics, webhook, Prometheus scraping |
| Kubernetes |
make demo/kind |
kind deploy, Secret/Ingress discovery, metrics, webhook |
make demo/up # TLS origin + API + Prometheus + webhook receiver
make demo/verify # asserts API, metrics, webhook, Prometheus, and CLI
make demo/down # stop and remove volumes
The Kubernetes version reuses the kind + cert-manager flow:
make demo/kind
To switch the compose demo to real certificates (public or internal CA), drop
server.crt/server.key into demo/compose/certs/ and set DEMO_HOST to the
certificate DNS name. Details: demo/README.md. The
.github/workflows/demo.yml workflow runs both demos in CI.
Self-Signed Certificate Monitoring
Add CA certificates to trusted_cas for servers using self-signed or privately-signed certificates:
trusted_cas:
- '/etc/certificates/my-ca.pem'
Per-host CAs are also supported:
hosts:
- name: "Internal"
url: 'internal.example.com'
port: '443'
trusted_cas:
- '/etc/certificates/internal-ca.pem'
Supported modes:
check and check --watch — uses configured CAs
serve — uses CAs for all certificate fetches
Development
make test # Run all tests with race detector
make test/short # Fast tests (no race)
make test/cover # Coverage HTML report
make test/verbose # Verbose output
make lint # golangci-lint + go vet
make fmt # gofmt all source
make build # Build binary
make run # Build and run
make run ARGS="check" # Build and run with args
make tidy # Tidy and verify modules
make clean # Remove artifacts
Make Targets
make docker/build # Build Docker image
make docker/run # Build and run CLI in container
make docker/run/serve # Build and run API server in container
make compose/up # Start Docker Compose
make compose/down # Stop Docker Compose
make compose/logs # Follow logs
# Disposable Kubernetes dev environment (kind + cert-manager)
make dev/up # Build + start the disposable dev container
make dev/shell # Open a shell in the dev container
make dev/setup # Create kind cluster + cert-manager + deploy monitor
make dev/scan # One-off `k8s monitor` scan via the DaemonSet
make dev/alert-demo # Metrics + webhook alert demo
make dev/logs # Stream the deployed monitor logs
make dev/down # Stop the dev container
make dev/destroy # Remove container + cluster state (zero residue)
# Deterministic demos (self-signed, no external DNS)
make demo/up # Start TLS origin + API + Prometheus + webhook
make demo/verify # Verify API, metrics, webhook, Prometheus and CLI
make demo/test # Start + verify
make demo/down # Stop and remove demo volumes
make demo/kind # kind + cert-manager end-to-end demo
Contributing
This project follows specific conventions to maintain code quality and consistency:
- Language: All code, comments, commits, and documentation must be in English
- Architecture: Follow SOLID principles lightly — focused responsibilities, dependency injection, clean interfaces
- Testing: Maintain ≥80% test coverage; never add production code without tests
- Validation: All changes must pass lefthook hooks (
pre-commit: lint + vet; pre-push: full test suite + coverage gate)
- Commits: Small, thematic, English commits following conventional format
For complete working conventions, invariants, and toolchain notes, see AGENTS.md.
License
Apache License 2.0 — see LICENSE file for details.
Author
Fabiano Florentino