Security software·Certificate daemon in production use

OpenVPN Management Suite

A certificate-lifecycle and VPN administration system designed around a narrow security boundary: an offline root CA establishes a constrained operational intermediate, while a dedicated local daemon mediates routine issuance, revocation, CRL generation, and client provisioning without exposing CA key material to the web application.

PKIX.509GoRustSvelteAF_UNIXOpenVPN
Type
Certificate-lifecycle & VPN administration system
Components
vpn-certd · vpn-certctl · ovpn-admin
Languages
Go · Rust · Svelte
Trust boundary
Local AF_UNIX socket · offline root CA

Every OpenVPN deployment past a handful of users needs the same six capabilities: issue certificates, enforce a profile on them, revoke the ones that go bad, regenerate and distribute a CRL, hand operators something they can actually install, and leave behind a record of what happened. The boring implementation puts all six inside the admin web app, because the web app already knows who’s logged in and what they clicked. I think that convenience is exactly the trap. A web app is cookies, sessions, templates, a database, and a dependency tree deep enough that nobody’s read all of it. It’s one of the most exposed processes in the whole system, and the design question worth asking first isn’t “how do we build this,” it’s “what happens the day this process gets popped.” vpn-certd starts from that question instead of ending on it.

IF THE CONTROL PLANE IS COMPROMISEDovpn-admin · web control planeoperator-facing, most exposedThe attacker inherits:Application sessions + databaseA fixed menu ofnamed daemon requestsAF_UNIXnamed requestsOUTSIDE THE BLAST RADIUSIntermediate signing keynever crosses the socketOffline root CAnever on a running hostOpenSSL shell · arbitrary FSno generic executionkey material never crossesthe boundary

Three authorities, three components, on purpose

Offline root CAsigns the operational intermediate onlymanual signingOperational intermediate CAheld by the certificate daemonvpn-certdissuance · revocation · CRLvpn-certctl / ovpn-adminoperator interfacesAF_UNIXOpenVPN environmentcertificates · client bundles · CRL

The suite splits into an offline root CA, an operational intermediate held by a dedicated daemon (vpn-certd), and operator-facing tooling (vpn-certctl for CLI, ovpn-admin for the Rust/Svelte web control plane) that talks to the daemon over a local Unix socket. Root authority exists only to stand up or replace the intermediate. Operational signing authority lives entirely inside the daemon. Operator workflow authority, deciding who’s allowed to ask for what, lives in the control plane. Splitting these doesn’t make any one of them uncrackable. It means popping the operator-facing layer doesn’t hand you the other two for free, which is the whole point.

The root stays out of the loop it doesn’t need to be in

The root key is never loaded by a running service. It only signs the intermediate, and routine operations, provisioning a user, renewing a client cert, revoking one, regenerating a CRL, issuing a server cert, never touch it. That buys a real reduction in exposure frequency at a real cost: replacing the intermediate is a deliberate manual procedure against offline material, which means that procedure has to actually be documented and rehearsed, not just assumed. An offline root nobody can restore is confidential and useless in the same breath. The payoff isn’t invincibility, it’s that an intermediate compromise doesn’t force you to re-establish trust on every client that’s ever connected.

The intermediate can sign certs. It cannot become an empire.

The operational CA carries a path-length constraint of zero, so it can issue client and server certificates but can never spin up a subordinate CA underneath itself. Everything else, common-name rules, validity windows, key size and type, key usage and extended key usage, duplicate-subject handling, gets enforced as policy rather than left to the UI’s good behavior. This matters because the common failure mode in small PKI setups is treating policy as a form-validation concern: the frontend blocks a bad common name, and the signing step downstream accepts whatever it’s handed anyway. Here the control plane can request an operation. Whether that operation is actually valid is the daemon’s call, not the caller’s.

vpn-certd does one job and nothing adjacent to it

The daemon, a single-purpose Go service, owns client and server certificate generation, CSR signing, revocation, CRL regeneration and retrieval, client-bundle assembly, and policy enforcement, full stop. It has no browser sessions, no HTML rendering, no general application workflows, no arbitrary database access, no client-side JS, no public HTTP routing to reason about. A narrow responsibility set doesn’t automatically mean the code is bug-free, but it means the surface you have to reason about and lock down is small enough to actually reason about. And the signing key never crosses the process boundary: the control plane sends named requests, not raw OpenSSL invocations, so a compromised caller inherits a fixed menu of operations, not a shell.

AF_UNIX instead of a TCP listener, because reachability is half the battle

The daemon exposes a local Unix socket, not a network port, which deletes an entire class of exposure: the CA service isn’t reachable just because an attacker can route to the host. Access control becomes socket ownership, group membership, file mode, service identity, and systemd unit permissions, letting the OS itself participate in enforcement instead of leaving it entirely to application logic. I want to flag the honest caveat here rather than skip it: a Unix socket is not security by itself. It’s weak the moment it’s world-writable, shared across unrelated service accounts, protocol-permissive about arbitrary commands, or sitting on a host that’s already fully owned. It’s one layer, and its value depends on everything else in this list being true alongside it.

Hardening the daemon like the privileged thing it actually is

The deployed daemon runs under a dedicated service account with NoNewPrivileges, private temp storage, a read-only view of most of the filesystem, home-directory protection, restricted executable memory, an empty capability bounding set, restricted namespaces, and no TCP/UDP surface at all, standard systemd hardening applied because the signing key gives this process real authority inside the VPN trust model even though it never needs Linux root. None of that stops every possible bug in the daemon. It bounds what a daemon-level compromise can reach outside the PKI state it already legitimately controls, and that’s a meaningfully smaller blast radius than “same host, same privileges as the web app.”

Two provisioning modes, and they are not interchangeable

vpn-certctl supports both server-side key generation (GENKEY_AND_SIGN, which the daemon generates and can bundle end to end) and signing an externally generated CSR (SIGN, where the private key never leaves the requester). Server-side generation is simpler to automate and gives consistent key parameters, at the cost of the private key briefly existing inside the provisioning system and bundle delivery becoming its own security-sensitive step. CSR signing means the CA never touches the requester’s key, at the cost of a more complex client workflow and CSR parsing/validation becoming a load-bearing part of the trust model. Which one you want depends on who controls the endpoint and whether it can be trusted to generate keys locally, not on which one is more convenient this week.

Revocation is not the sequel, it’s part of the same feature

A lot of certificate tooling gets built around the happy path of issuance and treats revocation as something to bolt on later. That’s a bad instinct for VPN access specifically, because the operational questions, which serial gets revoked, what reason gets recorded, when the CRL regenerates, how OpenVPN actually receives the update, what happens if publication fails, whether anyone’s watching CRL expiry, are exactly the ones that matter at 2am when someone’s laptop just got stolen. The suite treats issue and revoke as parallel first-class flows.

Issue
validate policy
sign
record metadata
create bundle
deliver to operator
Revoke
identify serial
record reason
update CA state
regenerate CRL
distribute to OpenVPN

A well-protected signing key attached to a revocation process nobody trusts is still a liability.

Audit records are an operational log today, not a tamper-proof ledger yet

Every certificate operation records subject, serial, timestamp, operation type, CSR fingerprint, and revocation reason where relevant, enough to answer who or what got issued a cert, which request produced it, when it was revoked, and whether unusual issuance patterns are showing up. I think it’s important to be precise about what this currently is: an operational record living on the same host the daemon runs on, which means a fully compromised daemon host can also alter its own audit trail. The honest next step, and one worth calling out explicitly rather than hand-waving past, is shipping security events off-host to a separately controlled log, so the audit trail survives the exact compromise scenario it exists to help you investigate.

Failing loudly beats failing open

If the daemon goes down, issuance and revocation stop, but certificates already issued keep working until they expire or get revoked elsewhere, an availability failure, not a silent trust failure. If socket permissions are wrong, authorized tooling loses access or unauthorized local processes gain it, which is exactly why socket ownership belongs in the deployment’s security configuration, not an afterthought. If CRL publication fails, a certificate can be marked revoked in CA state while OpenVPN is still trusting the old CRL, which is the argument for monitoring CRL age and publish success as seriously as you monitor issuance itself. If the intermediate is actually compromised, recovery means isolating the daemon, preserving evidence, cutting a new intermediate, reissuing affected certs, and pushing new trust and CRLs, the bounded-but-real incident this whole architecture was built to make survivable. And if the root material itself is unavailable when you need it, the intermediate can’t be replaced at all, which is the whole argument for treating offline storage as something with a tested recovery path, not encrypted media in a drawer nobody’s opened in two years.

Where things actually stand

vpn-certd and vpn-certctl are production-used: the offline-root-to-intermediate hierarchy, client and server issuance, CSR signing, revocation, CRL regeneration, OpenVPN CRL integration, client-bundle generation, and centralized policy enforcement are all live. ovpn-admin, the Rust and Svelte control plane, is still under active development, broader admin workflows, better operator visibility, fuller audit export, and more lifecycle automation are on the way but not finished. Worth saying plainly: the security-critical component is the one that’s actually done. The part that’s still evolving is the layer that was deliberately designed to hold the least authority in the system.

TL;DR

The design bet here is narrow and specific: a browser-facing application will eventually be compromised, so don’t let it be the thing holding the CA key. Push root authority offline, constrain the intermediate so a compromise doesn’t turn into an unbounded hierarchy, isolate signing in a small Go daemon reachable only over a Unix socket, and treat revocation and audit as load-bearing parts of the system rather than features you’ll get to later. None of that makes compromise impossible. It makes compromise bounded, which is the only promise a system like this can actually keep.

← back to work