Building Identity Infrastructure for a Home Agent Stack
I’ve been building a home AI agent platform — a small fleet of specialized agents that run locally, talk to each other over HTTPS, and do useful things. Getting the identity and trust layer right turned out to be the most interesting part of the project. This is the story of how I ended up building a small certificate authority, why I didn’t just use something off the shelf, and what I learned.
The setup: a self-managed microservice world
The platform, which I’m calling Hearth, is a collection of services running in Podman pods on a home server: a base agent framework (LLM + tools + the A2A protocol), specialized personas layered on top, and a handful of supporting services. Everything exposed to the network is authenticated with mutual TLS (mTLS), so both parties to every connection are known.
The key property I wanted: every service knows exactly who it is talking to, and every connection is between members of a defined organization. No ambient trust, no “anything on the LAN is safe.”
The agents themselves are participants in this, not just clients. An agent can spin up its own HTTPS endpoint, accept connections from other agents, and needs to verify that the caller is a legitimate peer and not something that wandered in off the network. That’s a different trust model from the usual “service authenticates users” pattern.
The problem with OIDC
The obvious first instinct is OAuth 2.0 / OIDC. It’s the industry standard, it’s well-understood, and there are libraries for it in every language.
But OIDC is designed around one specific flow: a human with a browser authenticates against an identity provider, gets redirected back to an app, and the app holds a short-lived token on behalf of that user. The whole dance — authorization codes, redirect URIs, the browser as the trust anchor — is built around that model.
Pull on any of those threads and it starts to fray:
CLI tools and scripts can’t do a browser redirect. The workarounds (device authorization flow, localhost redirect servers) are clunky and don’t compose well.
Agent-to-agent calls have no human in the loop at all. You end up with client credentials flow, which is essentially a shared secret dressed up in OAuth clothing. You’ve traded the complexity of the OIDC machinery for something that still needs a secrets management layer underneath.
Short-lived tokens help, but they mean you need token renewal, which requires either storing a refresh token (a long-lived secret) or hitting the identity provider constantly. For a home server with intermittent connectivity this gets painful fast.
The identity provider itself becomes a single point of failure. Every service needs to reach it to validate tokens. In a local microservice setup this is just latency and operational complexity you don’t need.
The pattern that kept showing up: OIDC ends up with tokens anyway — JWTs that carry claims, get validated, get passed around. The browser-redirect ceremony is the way you issue those tokens safely for human users. Take humans out of the loop and you’re doing a lot of work to get back to “service holds a credential that proves its identity.”
The solution: mTLS with org-level trust
Mutual TLS (mTLS) is the straightforward answer. Both sides of a connection present X.509 certificates. Both sides verify the other’s certificate against a trusted CA. If both certificates are signed by the same organizational CA, you have a closed system: every participant is a known member.
The properties this gives you are exactly what I needed:
No ambient trust. A connection without a valid org-signed certificate fails at the TLS handshake, before any application code runs. You can’t accidentally expose an endpoint.
Peer-to-peer, not hub-and-spoke. Once an agent has an org-signed cert, it can authenticate directly to any other agent. No identity provider needs to be consulted at runtime.
Cryptographic proof of possession. A client presenting a certificate is proving it holds the corresponding private key. This is stronger than a bearer token, which anyone who obtains it can use.
Decentralized validation. Services validate certificates against the org’s CA certificate, which is a static file. No network call required.
The tradeoff is that you need to manage the CA and the certificate lifecycle. For a large organization with thousands of services and employees this is a serious operational problem, which is why OIDC and its ecosystem exist. For a home server with a handful of services and a single operator, it’s a config file and a small service.
Designing the cert manager
The cert manager is a small FastAPI service backed by a MariaDB database. Its job is to be the organizational CA: sign certificates for new members, keep track of what it has signed, and support revocation.
The bootstrap problem
The one genuinely hard problem with mTLS is bootstrapping. How does a brand-new agent, which doesn’t have an org-signed cert yet, authenticate to the cert manager to get one?
The answer is a two-step flow with a single-use JWT as an intermediate:
An administrator (someone who already has access to the cert manager, either via their own org-signed cert or by running a command inside the pod) mints a single-use token for a specific common name — for example, requesting one for “Shino Sonnet.” The token is a JWT signed with a key derived from the CA’s private key, so it’s stable across restarts and tied to the CA identity.
The new member presents that token plus their self-signed identity certificate. That endpoint is deliberately public — no client cert required at the TLS layer — because the new member doesn’t have an org cert yet. The JWT is the credential that proves the admin authorized this enrollment. The cert’s identity must match the name in the token (anti-impersonation), and the token is single-use (anti-replay).
The cert manager signs the submitted certificate and returns a JSON introduction: the newly signed cert plus the CA cert. The new member installs this and can now participate in the org.
Tracking what we’ve signed
The database is simple: a tokens table (id, common name, used flag, issuer) and a
certificates table (serial, subject, issued/expires/revoked timestamps, client
address). Checking whether a given serial is still valid is the obvious check —
exists, not revoked, not expired.
Revocation with proof of possession
Revocation is interesting. Revoking a certificate requires mTLS, but also requires that the certificate the caller is presenting in the TLS handshake has the same serial number as the certificate they’re revoking. You can only revoke a cert by proving you hold its private key. Python’s SSL layer exposes the peer certificate’s serial number directly, so the check is a straightforward comparison.
This means a compromised credential can be revoked by whoever holds the private key — which, if the cert really is compromised, may not be its rightful owner. Admin-level override revocation is on the list (see below).
Current state
The service runs in a Podman pod alongside MariaDB. A bootstrap script handles first-run setup: provisioning the TLS identity volume (the CA keypair lives there), generating database credentials, and starting the pod.
There’s a small library that wraps the underlying X.509 mechanics: generating keypairs and self-signed certs, managing a directory of known clients and servers, running the web server with a custom SSL context, and providing HTTP client wrappers that automatically present the right certificate for each connection.
A working bootstrap sequence looks like:
bash bootstrap.sh
podman exec cert-manager-pod-cert-manager certified get-signer > cert-manager.json
certified init 'Your Name' --host localhost --host <your-ip>
certified add-service cert-manager:8443 cert-manager.json
# (admin mints a token inside the pod)
message -X POST "https://cert-manager:8443/certificates?ident=$(certified get-ident)" \
-H "Authorization: Bearer $(cat token.jwt)" > intro.json
certified set-org intro.json
After the last step, every connection to a registered service automatically presents the org-signed certificate, and the cert manager recognizes you as a known peer.
Future work
A few ideas are on the list for next time I’m back at this drawing board:
Replace the JWT signing key with a token format that supports attenuation. Right now the JWT secret is derived from the CA private key at startup. A token that carries its own signing key and supports scoped, narrower re-issuance would let an admin issue a token that can only be used from a specific IP range, or only during a time window.
Admin-level revocation. Currently only the cert owner can revoke their own cert. An admin with a designated CA-level cert should be able to revoke any cert — necessary for the scenario where a private key is actually compromised.
Expiry-aware token issuance. When signing a certificate, the issued lifetime should never exceed the remaining lifetime of the caller’s own cert. No point issuing a 1-year cert to someone whose authorization expires next week.
The infrastructure is working, and the design feels right. What’s left is refinement on a solid foundation — a good place to be.