PKI in a Box
(PIB)
A full internal Certificate Authority with ACME support, plus expiry monitoring for everything that does TLS. step-ca issues and renews your internal certs; pib-monitor watches what's about to expire.
Running your own internal PKI used to mean a weekend with OpenSSL, a fragile root key on a USB stick, and certs that all expire on the same Tuesday because you generated them at the same time. PIB packages step-ca as a full internal Certificate Authority with ACME support — so your internal services can request and auto-renew certs the same way they would from Let's Encrypt — and bundles pib-monitor for scanning TLS endpoints and pushing expiry metrics to VictoriaMetrics. Internal CA + cert monitoring, one compose file, no API keys, no cloud accounts.
⚡ Quick Start
git clone https://github.com/matijazezelj/pib.git && cd pib
cp .env.example .env # set CA_DNS_NAMES, GRAFANA_ADMIN_PASSWORD
make up # generates CA password + starts stack
On first start, make up generates a random CA password (ca/password.txt) and step-ca initialises a fresh root CA — takes about 10 seconds. Open Grafana at http://localhost:3005.
Save
ca/password.txt. It’s the only thing that unlocks your root key. Losing it means rebuilding the CA from scratch and re-bootstrapping every client.
🧠 What PIB Actually Is
Two halves that work together (or independently):
1. step-ca — the internal Certificate Authority
step-ca is a production-grade CA written by smallstep. PIB ships it with:
- Auto-initialised root + intermediate CA on first boot
- ACME endpoint so internal services can request and renew certs using the same protocol Let’s Encrypt uses
- JWK / OIDC provisioners for human and machine cert issuance (OIDC plays well with IIB)
- Sane defaults — 24-hour cert lifetimes for ACME, 1-hour for short-lived workloads, both fully configurable
- CRL + OCSP support so revocation actually works
Your internal services request certs from PIB the same way they’d request from any public CA. Then they auto-renew. Then they stop being something you think about.
2. pib-monitor — the cert expiry scanner
A lightweight collector that opens TLS connections to every endpoint in MONITOR_HOSTS every 6 hours, reads the certificate, and pushes expiry metrics to VictoriaMetrics. Doesn’t care who issued the cert — PIB’s own CA, Let’s Encrypt, internal self-signed, externally-issued — if it speaks TLS, pib-monitor can read it.
| Component | Technology | Purpose |
|---|---|---|
| Certificate Authority | step-ca (smallstep) | Issues and renews internal certs via ACME, JWK, OIDC |
| Cert Monitor | pib-monitor (Go) | Scans TLS endpoints, pushes expiry metrics every 6h |
| Storage | VictoriaMetrics | 365-day retention on cert health metrics |
| Visualization | Grafana (port 3005) | Pre-built expiry countdown dashboard |
🎯 Using the CA — Issuing Your First Cert
Once make up finishes, install the step CLI and bootstrap a client:
# Bootstrap the CA fingerprint so clients trust it
step ca bootstrap \
--ca-url https://ca.internal:9000 \
--fingerprint $(make ca-fingerprint)
# Request a cert via ACME (works for any service)
step ca certificate svc.internal svc.crt svc.key
# Or use the ACME endpoint directly — caddy, traefik, cert-manager all support this
# ACME directory: https://ca.internal:9000/acme/acme/directory
For services that already speak ACME — Caddy, Traefik, nginx with acme.sh, cert-manager — just point them at PIB’s ACME directory URL instead of Let’s Encrypt’s. Auto-renewal works the same way.
For human-issued certs (admin TLS, kubectl client certs, mTLS for ops), use the JWK or OIDC provisioner — and if you’ve got IIB running, OIDC issuance authenticates against your central identity with full audit trail of who requested what cert.
📈 Cert Monitoring (pib-monitor)
pib-monitor runs alongside the CA and scans whatever TLS endpoints you list in MONITOR_HOSTS. It’s the safety net that catches the certs you forgot you had — third-party services, externally-issued certs, internal services not yet on ACME.
Metrics Pushed
| Metric | Type | Labels | Meaning |
|---|---|---|---|
pib_cert_days_remaining |
gauge | host, cn, issuer |
Days until notAfter |
pib_cert_expiry_timestamp |
gauge | host, cn, issuer |
Unix timestamp of expiry |
pib_cert_subject_cn |
info | host, cn |
Subject Common Name |
pib_cert_sans |
info | host, sans |
Comma-separated SAN list |
pib_cert_issuer |
info | host, issuer |
Issuer DN |
pib_cert_is_expired |
gauge | host |
1 if past notAfter |
pib_cert_is_critical |
gauge | host |
1 if within CRITICAL_DAYS |
pib_cert_is_warning |
gauge | host |
1 if within WARN_DAYS |
pib_cert_is_not_yet_valid |
gauge | host |
1 if notBefore is in the future |
These are real metrics, not log lines — alert on them in Grafana, Alertmanager, or anything else that speaks PromQL/MetricsQL.
What pib-monitor Sees
- HTTPS endpoints (
host:443) - Custom-port services (
host:8443,mailhost:465,ldaphost:636) - IPv6 endpoints (
[2001:db8::1]:443) - Bare IPs (when you don’t trust DNS or you’re checking the cert directly)
- Internal CAs including step-ca itself — get warned before your root expires and silently kills every issued cert
- SNI-routed services — SNI sent based on the hostname you provide
📊 Dashboard
The bundled Grafana dashboard gives you:
| Panel | What it shows |
|---|---|
| Fleet cert health | Single-glance counts: OK / Warning / Critical / Expired |
| Expiry timeline | All monitored certs plotted by notAfter — see clusters of expirations coming up |
| Cert detail table | Host, CN, issuer, days remaining, status — sortable, filterable |
| By issuer breakdown | Let’s Encrypt vs. PIB internal CA vs. self-signed at a glance |
| Trend lines | Watch days-remaining tick down so renewals don’t surprise you |
| CA health | step-ca uptime, ACME request rate, issuance + renewal counts |
Default thresholds: warning at 30 days, critical at 7 days. Both configurable.
⚙️ Configuration
| Variable | Default | Purpose |
|---|---|---|
CA_DNS_NAMES |
(required) | DNS names the CA cert is valid for (e.g. ca.internal,pki.internal) |
GRAFANA_ADMIN_PASSWORD |
(required) | Grafana admin password |
GRAFANA_PORT |
3005 |
Host port for Grafana |
STEP_CA_PORT |
9000 |
Host port for the step-ca HTTPS endpoint |
MONITOR_HOSTS |
(empty) | Comma-separated host:port list for pib-monitor |
WARN_DAYS |
30 |
Days remaining that triggers the warning state |
CRITICAL_DAYS |
7 |
Days remaining that triggers the critical state |
SCAN_INTERVAL_HOURS |
6 |
How often pib-monitor re-probes every host |
VICTORIAMETRICS_URL |
http://victoriametrics:8428 |
Where pib-monitor pushes metrics |
Example: production CA + monitoring
CA_DNS_NAMES=ca.internal,pki.internal
GRAFANA_ADMIN_PASSWORD=...
MONITOR_HOSTS=api.example.com:443,admin.example.com:443,grafana.example.com:443,mail.example.com:465,ldap.example.com:636,[2001:db8::10]:443
WARN_DAYS=45
CRITICAL_DAYS=14
SCAN_INTERVAL_HOURS=2
🔔 Alerting
PIB pushes metrics; you alert in whatever you already use. Sample Grafana / Alertmanager rules:
# Critical: any cert expires in less than 7 days
pib_cert_days_remaining < 7
# Warning: bulk renewal coming up
count(pib_cert_days_remaining < 30) > 5
# CA root expiry — this matters more than the rest
pib_cert_days_remaining{cn="PIB Root CA"} < 90
Wire that into Alertmanager, Grafana OnCall, ntfy, or the Telegram bot you’ve already got running.
👥 Who This Is For
- Anyone tired of self-signed certs everywhere in their homelab or internal network
- Teams running internal services that need real TLS but can’t (or shouldn’t) use public LE certs for
*.internal - Homelabbers who want their internal services to auto-renew via ACME the same way their public services do
- Ops teams managing dozens of internal services where Let’s Encrypt isn’t an option (private DNS, air-gapped, etc.)
- SREs who need cert telemetry as a first-class signal next to uptime and latency
- Anyone running mTLS at any scale — that’s a lot more certs than you think
Who This Is NOT For
- Teams running only LE-issued public certs with cert-manager and zero internal PKI — your renewal story is already automated
- Anyone needing a CA with formal WebTrust audit / public trust — PIB issues internal-only certs, by design
- Organisations under strict HSM / FIPS requirements — step-ca supports HSMs but PIB’s defaults don’t configure one
🔗 Pairs Well With
- IIB — wire IIB as the OIDC provisioner so cert issuance is authenticated against your central identity
- AIB — AIB tracks certs discovered from infrastructure; PIB actively probes the endpoints and issues new ones
- XIB — bundled with VIB + CIB + TIB in the umbrella suite
- SIB — detect attacks against your TLS endpoints, monitor the certs protecting them
📄 License
Apache 2.0 License — use it, modify it, build on it.
Ready to run your own internal CA?
View on GitHub🚀 Need Help Getting Started?
Self-hosting is free and always will be. But if you'd rather have it deployed, configured, and maintained for you — I can help.