The WiFi Certificate That Didn't Exist an Hour Ago | Neil HanlonSkip to content<br>~/posts<br>The WiFi Certificate That Didn't Exist an Hour Ago<br>I taught swamp to drive FreeIPA, then used it to issue my laptop an 802.1X client cert and get onto my own network. Mostly this is about knowing when to write things down.<br>July 10, 2026<br>6 min read
My laptop is on WiFi right now on a certificate that didn’t exist an hour ago. I<br>made the keypair, my own CA signed it, the private key went into my secret store,<br>and the machine authenticated onto the 802.1X network and pulled a DHCP lease —<br>all through code I’d finished writing about twenty minutes earlier. I want to<br>talk about the boring part that made it safe, because the boring part is the only<br>part I’d defend.<br>The itch<br>I run FreeIPA at home. Kerberos realm, LDAP, a CA —<br>the same identity stack plenty of companies run, except mine is operated entirely<br>by hand. kinit, the ipa CLI, a shell script when I’m feeling fancy. Every<br>cert, every group, every bit of it is me typing commands and hoping I remembered<br>the flags. It works, and it’s also exactly the sort of thing I’ll fat-finger at<br>11pm.<br>I wanted it declarative: describe an identity object, have something reconcile<br>it, keep a record of what it did, and let me wire one piece of the homelab into<br>the next.<br>swamp, quickly<br>swamp models real resources as typed<br>objects with methods — create, read, mutate — and keeps every run as versioned<br>data other models can read. You write extensions in TypeScript. It’s the good<br>parts of infrastructure-as-code without the pretense that the world holds still.<br>FreeIPA had nothing in the registry, so I got to start from scratch, which is my<br>favorite way to start.<br>Not one model, a family<br>FreeIPA is a platform, not a service, so I split it into a few small packages<br>instead of one enormous one. @shrug/freeipa/domain is read-only: it logs into<br>the JSON-RPC API and snapshots the realm, the server inventory, the replication<br>topology. Understand the domain before you touch it.<br>Then the ones that write: user, group, and cert. Certs got their own<br>package on purpose — in FreeIPA a certificate can belong to a user, a host, or a<br>service, so hanging cert issuance off the “user” model would have been wrong the<br>second I wanted a device cert. (I wanted a device cert.)<br>I built the three write packages at the same time, in parallel: three agents,<br>each in its own git worktree, each cloning the shape of the read-only package and<br>each carrying an identical copy of the same “write-kernel.”<br>What the write-kernel actually is<br>That last bit needs unpacking, because “byte-for-byte identical write-kernel” is<br>the kind of phrase that sounds like it means something and might not. It’s not a<br>shared library — each package vendors its own copy. It’s a small module that<br>every write goes through: it takes the mutation you want, runs it, and decides<br>what gets persisted and when. Same code, same rules, in all three packages, so<br>“how do we write things down” gets answered once and copied, not reinvented three<br>times slightly differently. Why it’s worth copying is the next section.<br>The actual point: when do you write it down?<br>Here’s a question that sounds trivial and isn’t: when a mutating operation runs,<br>when do you record what happened?<br>swamp’s default guidance — and honestly most people’s reflex — is throw before<br>you write. If the op fails, persist nothing, because a stored record claiming a<br>success the world never delivered is a lie, and downstream models will read that<br>lie and act on it. That’s correct, and I kept it. But it quietly assumes every<br>kind of “what happened” wants to be written at the same instant, and two of them<br>don’t.<br>The first is irreplaceable material. When I issue a cert, the model generates<br>a fresh private key, sends the signing request, and the CA signs it. The cert<br>exists now. If something after that throws and I’d followed “throw before<br>writing,” I’d have thrown away the only private key that matches a certificate<br>that’s already, really, out in the world. So that one flips: write the<br>irreplaceable thing the moment it’s real, before anything else gets a chance to<br>fail.<br>The second is partial failure. One operation creates a steering group as both<br>a user-group and a host-group — two separate calls. If the first lands and the<br>second blows up, “it failed” is technically true and completely useless. What I<br>need to know is which half exists, so I can go fix the other one.<br>So the kernel follows three rules instead of a slogan:<br>State — the object itself — is written only on success. A failed mutation<br>never publishes state that says otherwise. (The original rule, untouched.)<br>Irreplaceable material — a generated key, a signed cert — is written the<br>moment it’s real, before any later throw can eat it.<br>An audit...