Add draft post: The WiFi Certificate That Didn't Exist an Hour Ago
Narrative on building the @shrug/freeipa/* swamp extension family and the write-safety pattern, closing the loop by issuing a laptop WiFi cert. Draft (draft: true); infra details kept generic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172sas9YuxtJpbaPx8aX9Uv
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: "The WiFi Certificate That Didn't Exist an Hour Ago"
|
||||||
|
description: "I built a family of FreeIPA automation extensions for swamp, then used them to mint my laptop an 802.1X client certificate and walk onto the network — end to end, through code I'd just finished writing. A note on the write-safety pattern that made it safe."
|
||||||
|
date: 2026-07-10T18:30:00-04:00
|
||||||
|
draft: true
|
||||||
|
categories: ['infrastructure', 'automation']
|
||||||
|
tags: ['freeipa', 'swamp', 'pki', '802.1x', 'eap-tls', 'radius', 'homelab', 'devops']
|
||||||
|
---
|
||||||
|
|
||||||
|
My laptop is on WiFi right now with a client certificate that did not exist an
|
||||||
|
hour ago. I generated the keypair, my own certificate authority signed it, the
|
||||||
|
private key went straight into encrypted storage, and the machine walked onto
|
||||||
|
the 802.1X network and pulled a DHCP lease — all through a tool I had just
|
||||||
|
finished writing. This is a short note on how that came together, and on the one
|
||||||
|
piece of engineering I think is actually worth sharing.
|
||||||
|
|
||||||
|
## The itch
|
||||||
|
|
||||||
|
I run [FreeIPA](https://www.freeipa.org/) at home — my own Kerberos realm, LDAP
|
||||||
|
directory, and certificate authority, the same identity stack a lot of companies
|
||||||
|
run in the large. It is a genuinely great piece of software. It is also, in my
|
||||||
|
setup, operated entirely by hand: `kinit`, the `ipa` CLI, a shell script when I
|
||||||
|
am feeling fancy. Every user certificate, every group, every bit of it is a
|
||||||
|
human typing commands and hoping they remembered the flags.
|
||||||
|
|
||||||
|
I wanted it declarative. I wanted to describe an identity object and have
|
||||||
|
something reconcile it, keep a versioned record of what happened, and let me
|
||||||
|
wire one piece of infrastructure into the next.
|
||||||
|
|
||||||
|
## What swamp is, briefly
|
||||||
|
|
||||||
|
[swamp](https://github.com/swamp-club/swamp) models real resources as typed
|
||||||
|
objects with methods — create, read, mutate — and captures every run as
|
||||||
|
versioned data you can reference from other models. You extend it in TypeScript.
|
||||||
|
Think of it as the good parts of infrastructure-as-code without pretending the
|
||||||
|
world is a static graph. FreeIPA had no coverage in the registry, so this was
|
||||||
|
greenfield.
|
||||||
|
|
||||||
|
## A family, not a monolith
|
||||||
|
|
||||||
|
FreeIPA is a platform, not a service, so I split the work into a family of small
|
||||||
|
packages rather than one god-model. The first, `@shrug/freeipa/domain`, is
|
||||||
|
read-only: it logs into the JSON-RPC API and snapshots the realm, the server
|
||||||
|
inventory, and the replication topology. "Understand the domain" before you
|
||||||
|
touch it.
|
||||||
|
|
||||||
|
Then the interesting ones, the packages that *write*: `user`, `group`, and
|
||||||
|
`cert`. I split certificates into their own package on purpose — in FreeIPA a
|
||||||
|
certificate can be issued to a user, a host, or a service, so binding cert
|
||||||
|
issuance to the "user" model would have been wrong the moment I wanted a device
|
||||||
|
cert.
|
||||||
|
|
||||||
|
I built the three write packages in parallel — three agents, three repositories,
|
||||||
|
each mirroring the structure of the read-only one and each carrying a
|
||||||
|
byte-for-byte identical "write-kernel." That shared kernel is where the real
|
||||||
|
thinking went.
|
||||||
|
|
||||||
|
## The part worth sharing: how do you safely persist a mutation?
|
||||||
|
|
||||||
|
Here is a question that sounds trivial and is not: when a mutating operation
|
||||||
|
runs, *when* do you write down what happened?
|
||||||
|
|
||||||
|
The default guidance — swamp's, and honestly most people's instinct — is **throw
|
||||||
|
before you write**. If the operation fails, persist nothing, because a stored
|
||||||
|
record that claims success the world did not deliver is a lie that downstream
|
||||||
|
consumers will read and act on. That rule is correct, and I kept it. Mostly.
|
||||||
|
|
||||||
|
Because two cases quietly break its assumption.
|
||||||
|
|
||||||
|
**Irreplaceable material.** When I issue a certificate, the model generates a
|
||||||
|
fresh private key, sends the signing request, and the CA signs it. The
|
||||||
|
certificate now *exists*. If any step after that throws and I had followed "throw
|
||||||
|
before writing," I would lose the only private key that matches a certificate
|
||||||
|
that is already real. Here the safe-sounding rule is actively destructive. The
|
||||||
|
right principle is the opposite: persist irreplaceable state the instant it
|
||||||
|
becomes real, before anything else can fail.
|
||||||
|
|
||||||
|
**Partial failure.** One of the operations creates a steering group as *both* a
|
||||||
|
user-group and a host-group — two independent calls. If the first lands and the
|
||||||
|
second errors, "it failed" is true but useless; what you need to know is which
|
||||||
|
half exists so you can reconcile it.
|
||||||
|
|
||||||
|
So the kernel follows a three-way rule instead of a slogan:
|
||||||
|
|
||||||
|
1. **State** — the object itself — is written only on success. A failed mutation
|
||||||
|
never publishes misleading state. (The original rule, intact.)
|
||||||
|
2. **Irreplaceable material** — a generated key, a signed cert — is persisted the
|
||||||
|
moment it is real, before any later throw can eat it.
|
||||||
|
3. An **audit record** is written on *both* paths, success and failure. It is
|
||||||
|
telemetry, not state; when it says `success: false` it is telling the truth,
|
||||||
|
so persisting it on failure violates nothing.
|
||||||
|
|
||||||
|
The slogan is right until the domain proves it wrong in a specific, nameable
|
||||||
|
way. Then you deviate on purpose, and you write down why.
|
||||||
|
|
||||||
|
## The payoff
|
||||||
|
|
||||||
|
With that in place the actual automation is almost boring, which is the goal.
|
||||||
|
One method idempotently ensures the VLAN-steering group exists as both a
|
||||||
|
user-group and a host-group, swallowing "already exists" so re-runs are safe.
|
||||||
|
Another generates an RSA keypair and a PKCS#10 request in-process, submits it to
|
||||||
|
the CA, and stores the resulting private key **encrypted in my secret store** —
|
||||||
|
never in plaintext, never in the model's own data.
|
||||||
|
|
||||||
|
Then the last mile, by hand because it should be: pull the signed certificate
|
||||||
|
and the key out to files, point NetworkManager's EAP-TLS profile at them, and
|
||||||
|
bring the interface up. The RADIUS server validated the certificate against my
|
||||||
|
CA, said yes, dropped me on a VLAN, and DHCP did the rest. I was online — on an
|
||||||
|
SSID with a potato in the name, because this is *my* network and I make the
|
||||||
|
rules.
|
||||||
|
|
||||||
|
A certificate that did not exist when I started the afternoon, issued by
|
||||||
|
infrastructure I automated the same afternoon, carrying a laptop onto the
|
||||||
|
network. That is the whole loop, closed.
|
||||||
|
|
||||||
|
## Why bother
|
||||||
|
|
||||||
|
I could have typed the `ipa` commands. I have, many times. But automating a thing
|
||||||
|
is how you are forced to actually understand it — every flag you were cargo-culting,
|
||||||
|
every failure mode you were getting away with ignoring. The write-safety pattern
|
||||||
|
above did not come from wanting clever code; it came from asking "what happens if
|
||||||
|
this throws *here*" until the answer stopped being "you lose your private key."
|
||||||
|
|
||||||
|
The packages are open source on the swamp registry. If you run FreeIPA and want
|
||||||
|
to stop operating it by hand — or if you just like arguing about when to persist
|
||||||
|
a failure — come find me.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
I do this kind of infrastructure and automation work through
|
||||||
|
[my consulting practice](https://shrugpw.com), and I am around the usual places
|
||||||
|
on the internet.
|
||||||
Reference in New Issue
Block a user