🔐

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.

Want this deployed and maintained for you? Review the engagement options

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.