Budget PKI: OpenBao and Step CA on a Raspberry Pi

I’ve been running my own certificate authority for over a year now. Not a toy CA with openssl req and a self-signed root sitting in a directory somewhere — a real, automated PKI that issues short-lived TLS certificates to services across my homelab, handles Kubernetes ingress, and auto-renews everything without me touching it. The whole thing runs on a Raspberry Pi 3 that cost me $35 and a Nitrokey HSM that ran about $200.

This isn’t a tutorial. It’s the story of why I built it, what the architecture looks like, and what I learned doing it with open-source tools on budget hardware.

Why Bother

If you run a homelab with more than a handful of services, you’ve probably hit the TLS problem. You either slap self-signed certs everywhere and click through browser warnings, use Let’s Encrypt for everything (which means your internal services need public DNS), or you just don’t encrypt internal traffic and hope nobody’s sniffing your VLAN.

None of these are great. I wanted internal services to have proper TLS with short-lived certificates that rotate automatically. I wanted Kubernetes workloads to get certs without manual intervention. And I wanted the root of trust to be something I control, not a third party.

I also wanted to learn how PKI actually works at an operational level. Reading about certificate chains is one thing. Running a CA that signs intermediates stored on hardware tokens and issues leaf certs to a Kubernetes cluster — that teaches you things no documentation can.

The Hardware

The CA runs on mitra, a Raspberry Pi 3 sitting on my server VLAN. It’s not fast. It doesn’t need to be. Certificate signing is a lightweight operation, and the Pi handles it fine.

The interesting piece is the Nitrokey HSM plugged into its USB port. This is a SmartCard-HSM — a hardware security module that generates and stores cryptographic keys on-device. The keys never leave the hardware. You can’t export them. You can’t dump them. If someone steals the SD card, they get nothing useful because the signing keys live on the Nitrokey.

I also have an Infinite Noise TRNG plugged in for hardware random number generation. A Pi’s entropy pool isn’t great, and when you’re doing cryptographic operations, good randomness matters. The TRNG feeds /dev/random via the infnoise service.

The Stack

Two main services run on mitra:

Step CA is the certificate authority itself. It’s an open-source CA from Smallstep that speaks ACME (the same protocol Let’s Encrypt uses) and can issue both X.509 and SSH certificates. My Step CA instance holds the intermediate CA key — an EC P-256 key generated directly on the Nitrokey HSM via PKCS#11. The key was generated with neverExtract policy, meaning it was born on the HSM and will die on the HSM.

OpenBao is the secrets manager — a community fork of HashiCorp Vault that happened after HashiCorp switched Vault to a non-open-source license. OpenBao handles secret storage, Kubernetes authentication, and policy-based access control. Its auto-unseal mechanism also uses the Nitrokey HSM — an RSA key on the device encrypts the master key, so when the Pi reboots, OpenBao unseals itself by talking to the HSM via PKCS#11 without any human intervention.

Both services are configured entirely in NixOS modules. No manual setup, no imperative configuration steps. If the SD card dies, I flash a new one, deploy the NixOS configuration, plug in the Nitrokey, and everything comes back.

The Trust Chain

BPH Root CA (RSA-4096, offline, 20-year validity)
└── BPH Step-CA Intermediate (EC P-256, on Nitrokey HSM, 10-year validity)
    ├── Service TLS certificates (14-day default, auto-renewed)
    ├── Kubernetes ingress wildcards (24-hour, auto-renewed)
    └── SSH host/user certificates

The root CA key is RSA-4096, generated offline, and not stored on any running system. The root signed the intermediate, and the intermediate lives on the Nitrokey. Every NixOS host in my fleet trusts the root CA PEM through security.pki.certificates, which means any certificate issued by Step CA is automatically trusted system-wide.

Short-lived certificates are the whole point. My Kubernetes ingress wildcard cert (*.k8s.bph) has a 24-hour lifetime and renews 8 hours before expiry. OpenBao’s own TLS cert renews every 12 hours via a systemd timer that calls step ca certificate. If Step CA goes down, everything keeps working for at least 16 hours while I fix it. If it stays down longer, certs start expiring and services start failing — which is the correct behavior. A PKI that fails open is worse than no PKI at all.

Kubernetes Integration

This is where the setup pays for itself. Every service in my K3s cluster gets TLS certificates automatically through cert-manager and the step-issuer controller.

The flow works like this:

  1. A workload declares a Certificate resource requesting a cert for, say, pg.k8s.bph
  2. cert-manager sees it and delegates to the StepClusterIssuer
  3. The step-issuer controller authenticates to Step CA using a JWK provisioner
  4. Step CA signs the certificate using the intermediate key on the Nitrokey HSM
  5. cert-manager stores the cert as a Kubernetes Secret
  6. The workload mounts the Secret and serves TLS
  7. cert-manager renews it before expiry — no human involved

For secrets (API keys, database passwords, tokens), external-secrets-operator pulls from OpenBao. Pods authenticate to OpenBao using projected service account JWTs with audience-bound tokens — Kubernetes signs the token, OpenBao validates it against the cluster’s token reviewer API, and the pod gets only the secrets its policy allows. No long-lived tokens sitting in ConfigMaps.

The provisioning for all of this — the Kubernetes auth backend, the policies, the secret mounts — is managed in Terranix (Terraform written as Nix), so the OpenBao configuration itself is declarative and version-controlled alongside everything else.

Secrets Management with sops-nix

NixOS has a problem: the Nix store is world-readable. You can’t put secrets in your NixOS configuration because anyone on the system can read /nix/store. The solution is sops-nix, which encrypts secrets at rest in your Git repository using age keys derived from each host’s SSH key.

Each host in my fleet has its own age key. The .sops.yaml file maps which hosts can decrypt which secret files. Mitra’s secrets (the HSM PIN, backup credentials) are only decryptable by mitra’s age key. Kubernetes secrets are decryptable by the K3s nodes. At NixOS activation time, sops-nix decrypts the relevant secrets and writes them to /run/secrets/ with appropriate permissions.

The HSM PIN for OpenBao’s auto-unseal gets injected this way:

sops.templates."openbao-env" = {
  content = ''
    BAO_HSM_PIN=${config.sops.placeholder.openbao_hsm_pin}
  '';
  owner = "root";
  mode = "0400";
};

The PIN never appears in the Nix store, never appears in ps output, and only exists as an environment variable in OpenBao’s systemd unit. The HSM enforces its own PIN retry lockout, so even if someone got the PIN, they’d need physical access to the Nitrokey.

Operational Reality

It mostly just works. The 15-minute health check timer catches HSM hiccups (usually pcscd needing a restart after a USB blip) and auto-recovers. Backups run every 6 hours for OpenBao’s Raft snapshots and daily for Step CA’s database, both going to S3 via restic with 7-daily/4-weekly/12-monthly retention.

The main operational concern is the Nitrokey itself. If it dies, I need to re-initialize a new one with fresh keys, re-sign the intermediate from the offline root, and update the OpenBao unseal configuration. I keep a spare Nitrokey for this reason. The procedure is documented but I haven’t had to use it yet.

What It Cost

ComponentCost
Raspberry Pi 3~$35
Nitrokey HSM 2~$200
Infinite Noise TRNG~$50
MicroSD card~$15
Total~$300

The software is entirely open-source. No licensing fees. No subscriptions. Compare this to HashiCorp Vault Enterprise (which you’d need for HSM auto-unseal in the proprietary version) or any commercial CA solution.

Was It Worth It

Absolutely. Not because running your own PKI on a Raspberry Pi is the most practical thing in the world — it’s not, and if you just need certs, Let’s Encrypt with a DNS challenge works fine for most people. It was worth it because I now understand PKI operationally in a way I never would have from just reading about it.

I understand why short-lived certificates matter. I understand what happens when a CA goes down. I understand the difference between a root that signs an intermediate and an intermediate that signs leaves, not as an abstract concept but as a thing I’ve debugged at 2 AM when a timer didn’t fire. I understand why HSMs exist and what they actually protect against.

If you run a homelab and you want to learn security infrastructure, build a CA. Use real tools, not toy scripts. Put the keys on hardware. Automate the renewal. Break it and fix it. That’s how you learn what enterprise teams are actually dealing with.