Trust No One: Building an Internal PKI, Identity Platform, and Secrets Vault

authentikvaultwardenstep-capkiidentitysecurity

At some point in every home lab’s evolution, you hit the wall. Not a hardware wall — you can always buy more RAM. The wall is the moment you realise your entire security posture consists of self-signed certificates that every browser complains about, passwords stored in a spreadsheet, and “authentication” that amounts to hoping nobody else is on your network.

The answer to this wall is a dedicated identity VM. We called it DebIdentity, and it runs three services that, once you have them, make you wonder how you tolerated the alternative.

The Three Pillars

Step-CA — Internal Certificate Authority. Issues TLS certificates for every service in sdx.local that are trusted by every device in the lab, eliminating browser warnings and enabling real mutual TLS.

Authentik — Identity Provider. Central authentication for all VMs and services via LDAP, OIDC, and SAML. One login, everywhere.

VaultWarden — Password vault. Compatible with the Bitwarden client ecosystem. Stores every credential, API key, and secret in the lab, backed by SSO via Authentik.

All three run in Docker on DebIdentity, a Debian 12 VM at 172.16.50.3. Traefik handles TLS termination using Step-CA certificates, which means every service behind it gets a trusted HTTPS endpoint automatically.

Step-CA: Making Browsers Stop Complaining

The smallstep Step-CA is a production-grade internal Certificate Authority that runs as a Docker container and issues certificates on demand. The setup involved:

  1. Generating a root CA keypair (“SDX Lab CA Root CA”)
  2. Distributing the root CA certificate to the Windows trust store on REX, the macOS trust store on other machines, and every VM’s system trust store
  3. Configuring Step-CA to issue certificates with a 3-year validity period (26280 hours)

That last point deserves explanation. Internal CAs running in homelabs have a rotation problem: if you issue 90-day certificates like Let’s Encrypt does, you need automation to renew them, and automation that fails at 3am means your entire lab is down when you wake up. Three-year validity is a deliberate tradeoff — slightly less “correct” from a security textbook perspective, and considerably less likely to ruin a Sunday morning.

The certificates issued cover: litellm.sdx.local, auth.sdx.local, vault.sdx.local, OPNsense, Aruba, and the Synology NAS. Every service in the lab now has TLS that browsers, CLI tools, and Python’s requests library all trust without additional flags.

Authentik: Centralised Identity

Authentik is an open-source identity provider that supports LDAP, OIDC, SAML, and RADIUS. For a home lab, this is significant overkill. This is the correct amount of overkill.

The deployment runs Authentik server + worker containers, backed by PostgreSQL and Redis. The integration surface covers:

LDAP outpost (port 389/636): Every Linux VM runs SSSD configured to authenticate users against Authentik’s LDAP outpost. This means the lab owner’s account is a real user on every VM without local accounts being duplicated manually.

RADIUS outpost (ports 1812-1813/UDP): Juniper switches, OPNsense, and the Aruba wireless controller all authenticate network device logins against Authentik via RADIUS. SSH into the Juniper core switch uses the same credentials as logging into a VM.

OIDC for VaultWarden: VaultWarden’s login page redirects through Authentik for SSO. You authenticate once to access both the identity layer and the secrets layer.

Service accounts in Authentik follow a clear naming convention — short, descriptive, scoped to function:

  • An AI infrastructure account for SSH operations against network devices (read-only JunOS access by default)
  • A local service account for the AI assistant, deliberately not Authentik-managed to avoid circular dependencies at boot
  • A dedicated VaultWarden CLI account for API key access from WSL2 scripts

The claude and ai-agent accounts that existed in early lab iterations were deprecated once it became clear that generic AI agent accounts with undefined scope are a security smell. Named, scoped accounts only.

VaultWarden: Secrets Done Right

VaultWarden is a Bitwarden-compatible server that runs in Docker and provides a full password management solution with browser extension support, TOTP, and organisations for sharing credentials.

Every credential in the lab lives here:

  • Network device passwords (Juniper, OPNsense, Aruba, Synology)
  • API keys (LiteLLM master key, GCP service account keys, Gemini API key)
  • SSH private keys for service accounts
  • OIDC client secrets for Authentik integrations
  • The WiFi password (yes, even that)

Automated secret retrieval works via the Bitwarden CLI running as vault-cli@sdx.local in WSL2. MCP server start scripts call vault-get-secret.sh before starting, pulling credentials at runtime rather than storing them in environment files. The env file fallback (~/.opnsense-env) exists for disaster recovery when VaultWarden is unavailable — but VaultWarden being unavailable is a DebIdentity problem, and DebIdentity has a UPS.

The Use Case: Eliminating Credential Sprawl

Before DebIdentity, the lab’s secret management strategy was a collection of environment files, a shared password in a notes app, and at least one hardcoded API key in a config file that shall not be named.

After DebIdentity, every service authenticates via a named account, every API key lives in VaultWarden behind MFA, and rotating a credential means updating one record rather than hunting through a dozen config files. The MCP server for OPNsense pulls its API key from VaultWarden at startup. The LiteLLM proxy config references the master key via environment injection. The Claude Code configuration reads the LiteLLM key from the same VaultWarden record.

The cognitive overhead of managing secrets dropped significantly. The security posture improved in proportion.

The Uncomfortable Truth About Internal PKI

The part nobody tells you about running your own CA: the first thing you have to do is distribute the root certificate to every device that needs to trust it. On Windows, this is a Group Policy or a manual import into the certificate store. On macOS, it’s a similar process via Keychain. On every Linux VM, you add it to /usr/local/share/ca-certificates/ and run update-ca-certificates.

This takes about an afternoon. It involves a USB drive and more scp commands than you’d like. The result — every browser, every curl, every Python script trusting your internal certificates without complaint — is worth every minute of it.

The alternative is living with curl --insecure flags in your automation scripts, which is the infrastructure equivalent of a smoke detector running on an AA battery you’ve been meaning to replace.


Update — August 2026

Of the three pillars, one is gone, one is unchanged, and one is doing less than this post claims.

VaultWarden was decommissioned on 11 June 2026. The container, its data, the Authentik OIDC application, the vault-cli service account and the Bitwarden CLI were all removed. Lab credentials now live in a single age-encrypted file on the host, decrypted with a passphrase when a task needs it, with a small helper script for adds and updates so nothing lands in shell history.

That sounds like a downgrade and mostly isn’t. The MCP start-up scripts described above — vault-get-secret.sh piping a secret into an environment file at launch — were a moving part that broke whenever the vault container did, in service of a threat model (browser extension, TOTP, credential sharing across users) that a single-operator lab doesn’t have. One encrypted file is fewer things to keep running. The trade-offs are real, though: there’s no audit trail on reads, and secrets that need to be available to unattended scheduled tasks can’t be in there at all — the Telegram bot token and a copy of the switch SSH key sit outside it as ACL-locked files, because a scheduled task can’t type a passphrase at 7:55am.

Step-CA is untouched and still the best thing on this VM. It runs on port 9443 on DebIdentity with a JWK provisioner, and every device management endpoint in the lab — firewall, NAS, wireless controller — serves a certificate it issued. A cert audit in June found the wireless controller quietly serving its factory self-signed certificate, because its web-server profile pointed at a certificate name that didn’t exist; it had presumably been doing that for months without anyone noticing, which is what “just click through the warning” costs you. The new core switch sidesteps the question entirely: its web management interface is disabled, so it’s SSH and NETCONF only and there’s no web certificate to get wrong.

Authentik is doing less than advertised. Two of the three integrations in this post have quietly stopped carrying load:

  • RADIUS is effectively broken. The Aruba controller’s management authentication still points at the Authentik RADIUS outpost, and that server group never accepts — its counters read zero accepts against a hundred rejects and 152 timeouts. With a five-second timeout and three retransmits, every single management login to the wireless controller, including the MCP server’s, burns about twenty seconds before falling back to the local account. It silently broke the weekly config backup in July until the script’s login timeout was raised. Slow Aruba logins in this lab are expected behaviour, which is a deeply unsatisfying sentence.
  • The switch no longer uses it at all. RADIUS was dropped from the core during the emergency rebuild in June and never re-added. Switch access is now a read-only local account for the MCP server and a break-glass super-user account for changes.
  • LDAP is alive and was hardened. Every Linux VM still authenticates via SSSD against the LDAP outpost, and as of 7 July 2026 it runs over LDAPS on 636 end to end; cleartext 389 is no longer published.

The service-account convention held up well. The AI automation account is an Authentik-managed LDAP identity, with a deliberate local break-glass account kept alongside it on each host — because the failure mode this post worried about (identity provider down, nothing can authenticate) is exactly the one you plan for rather than argue with.