Rewrite both AI drafts in Neil's voice (still draft)
De-AI'd both posts per editorial review: cut negative parallelisms, manufactured section-enders, twin sign-offs, and excess em-dashes; added hedges and rough edges. Sign-offs now plain and distinct. - Swamp: real Baby Buddy numbers (96 feedings / 65 sleeps / 4h46m); cut the fabricated vitamin-D-three-spellings anecdote, replaced with the true midnight/2-3am dosing cluster; honest about not yet having the Pebble watch/ring hardware; added the real fork surface (FCM push, audit fields, soft-delete timers, new reports) - WiFi cert: added "what the write-kernel actually is" section; kept the parallel-agents-in-worktrees detail; framed the three-way write rule as his Still draft: true — pending Neil's technical accuracy pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "I Put My Son in a Swamp"
|
title: "I Put My Son in a Swamp"
|
||||||
description: "I modeled my newborn as a typed cloud resource — with a sync method, fifteen reports, and an idempotent delete. It is the fifth interface I have built for tracking one baby, and somewhere in the absurdity there is a real reason two sleep-deprived parents instrument everything."
|
description: "I modeled my newborn as a typed resource with a sync method and a stack of reports. It's the newest of five ways I track one baby, and somewhere in the absurdity there's a real reason two exhausted people instrument everything."
|
||||||
date: 2026-07-11T00:45:00-04:00
|
date: 2026-07-11T00:45:00-04:00
|
||||||
draft: true
|
draft: true
|
||||||
categories: ['automation', 'homelab']
|
categories: ['automation', 'homelab']
|
||||||
@@ -8,135 +8,133 @@ tags: ['swamp', 'babybuddy', 'parenting', 'home-assistant', 'mcp', 'pebble', 'de
|
|||||||
---
|
---
|
||||||
|
|
||||||
My son is a typed resource now. He has a `sync` method that pulls the last seven
|
My son is a typed resource now. He has a `sync` method that pulls the last seven
|
||||||
days of his life into a versioned snapshot, fifteen reports that summarize how he
|
days of his life into a versioned snapshot, a stack of reports that summarize how
|
||||||
slept and ate and — I want to be clear that I did think about this — a
|
he slept and ate, and — I did think about this — a delete that's careful to be
|
||||||
`delete-entry` method that is careful to be idempotent, because deleting the same
|
idempotent, because deleting the same diaper twice shouldn't be an error.
|
||||||
diaper twice should not be an error.
|
|
||||||
|
|
||||||
I did not set out to put a baby in a swamp. It just followed, the way these
|
I did not set out to put a baby in a swamp. It followed, the way these things do,
|
||||||
things do, from already having put everything else in one.
|
from having already put everything else in one.
|
||||||
|
|
||||||
## What "in a swamp" means
|
## What "in a swamp" means
|
||||||
|
|
||||||
[swamp](https://github.com/swamp-club/swamp) models real resources as typed
|
[swamp](https://github.com/swamp-club/swamp) models real resources as typed
|
||||||
objects with methods — read them, mutate them — and records every run as
|
objects with methods, and records every run as versioned data other models can
|
||||||
versioned data other models can reference. You write extensions in TypeScript. I
|
read. You write extensions in TypeScript. I used it [last week to automate my home
|
||||||
used it [last week to automate my home certificate
|
certificate authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}),
|
||||||
authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}), and the
|
and the whole time I kept thinking about the thing I actually spend all night
|
||||||
whole time I kept thinking about the thing I actually spend all night touching,
|
dealing with, which is not a certificate.
|
||||||
which is not a certificate.
|
|
||||||
|
|
||||||
We run [Baby Buddy](https://github.com/babybuddy/babybuddy) at home — a small,
|
We run [Baby Buddy](https://github.com/babybuddy/babybuddy) at home — a small,
|
||||||
self-hosted app for tracking a newborn's feedings, diapers, sleep, pumping,
|
self-hosted app for tracking a newborn's feedings, diapers, sleep, pumping, meds.
|
||||||
temperature, meds. It has a clean REST API. And a REST API, to a certain kind of
|
It has a clean REST API. A clean REST API, at 2am, to a certain kind of brain, is
|
||||||
tired brain at 2 a.m., is an invitation.
|
a dare.
|
||||||
|
|
||||||
So I wrote `@kneel/babybuddy`: one model that reads the whole instance into a
|
So I wrote `@kneel/babybuddy`: one model that reads the whole instance into a
|
||||||
snapshot and writes new entries back, plus a pile of reports that run over that
|
snapshot and writes new entries back, with a stack of reports that run over the
|
||||||
snapshot. Log a feeding, delete a mis-logged one, patch a bad timestamp. Then
|
snapshot. Log a feeding, fix a mis-logged one, patch a bad timestamp, then ask it
|
||||||
ask it questions.
|
questions.
|
||||||
|
|
||||||
## The reports tell on everyone
|
## The reports tell on you
|
||||||
|
|
||||||
The nice thing about summarizing a week of a baby is that the numbers are, on
|
Summarizing a week of a newborn produces numbers that are, on their own, a little
|
||||||
their own, quietly insane. Ninety-nine feedings. Sixty-five separate sleep
|
unhinged. In the last seven days: ninety-six feedings. Sixty-five separate sleep
|
||||||
sessions. Forty-four doses of medication. His longest unbroken stretch of sleep
|
sessions. His longest unbroken stretch was four hours and forty-six minutes,
|
||||||
was four hours and forty-eight minutes, which I know to the minute because there
|
which I know to the minute because there's a report called `sleep-longest-stretch`
|
||||||
is a report called `sleep-longest-stretch` and I run it the way other people
|
and I run it the way other people check the weather.
|
||||||
check the weather.
|
|
||||||
|
|
||||||
But the report I did not expect to matter was the medication one. I built it to
|
The one that got me was the medication report. I built it to answer "are we giving
|
||||||
answer "are we dosing the vitamin D on schedule," and instead it answered a
|
the vitamin D on schedule," and it answered a question I hadn't asked, which is
|
||||||
question I had not asked, which is "how many different ways has Neil spelled
|
"when does Neil actually remember the vitamin D." Not on schedule, it turns out.
|
||||||
vitamin D at midnight." The answer is three. `Vitamin D`, `Vitamin D Drops`, and
|
It's a little cluster of timestamps at midnight, 2am, 3am — whenever I surfaced
|
||||||
`Vitamin D drops` are, as far as the database is concerned, three unrelated
|
enough to think of it. The schedule in my head and the schedule in the database
|
||||||
substances, dosed on three overlapping schedules, by one man who was very tired.
|
were not the same schedule, and only one of them keeps records.
|
||||||
|
|
||||||
That is the honest value of instrumenting anything. Not the dashboard. The moment
|
That's the actual value of instrumenting anything: some night the dashboard hands
|
||||||
the dashboard shows you your own sloppiness back.
|
you back a picture of yourself you weren't planning to volunteer.
|
||||||
|
|
||||||
## This is not the first interface. It is the fifth.
|
## This isn't the first interface. It's the fifth.
|
||||||
|
|
||||||
Here is the part that I think is genuinely unhinged, and I say that with
|
Here's the genuinely stupid part, and I say that with affection for the man who
|
||||||
affection for the person who did it: the swamp extension is the *newest* way I
|
did it: the swamp extension is the *newest* way I have to track this kid, not the
|
||||||
have to track this child, not the only one, and not by a wide margin.
|
only one, and not by a wide margin.
|
||||||
|
|
||||||
There is a **mobile app** — a Capacitor wrapper around Baby Buddy with the sharp
|
There's a **mobile app** — a Capacitor wrapper around Baby Buddy with the sharp
|
||||||
edges filed off. Biometric lock so a phone left on the couch doesn't show the
|
edges filed down. Biometric lock, so a phone left on the couch doesn't show the
|
||||||
whole night's log. Siri intents, so "log a diaper" is a sentence you say into the
|
whole night's log. Log-a-diaper you can say out loud, because your hands are full,
|
||||||
air with both hands full, which is the only state your hands are ever in. Live
|
because your hands are always full. A running feed timer on the lock screen next
|
||||||
Activities, so a running feeding timer ticks on the lock screen next to the
|
to the elapsed time you're trying not to look at. iOS and Android, because of
|
||||||
elapsed time you are trying not to look at.
|
course iOS and Android.
|
||||||
|
|
||||||
There is a **Pebble watchapp**, because I have a Pebble again and it turns out the
|
There's a **Pebble watchapp**, because the best possible interface for logging a
|
||||||
best interface for logging a feed at 3 a.m. is a physical button on your wrist
|
feed at 3am is a physical button on your wrist that emits about four photons.
|
||||||
that does not emit a single photon more than necessary. Start a timer, stop it,
|
Start a timer, stop it, pick the amount, done, without the wake-your-whole-nervous-
|
||||||
pick the amount, done — without the wake-up-your-whole-nervous-system brightness
|
system glare of a phone. I'll admit the watchapp is currently ahead of my
|
||||||
of a phone. There is a Pebble smart *ring* on order for the same reason. I am not
|
hardware — I don't have the watch yet. There's a ring interface too. I don't have
|
||||||
going to defend the ring. The ring defends itself.
|
the ring yet either. I wrote software for two devices I'm still waiting on, which
|
||||||
|
tells you roughly everything about the state of mind here.
|
||||||
|
|
||||||
There is a **conversational server** — an MCP that lets me (or Claude, or the
|
There's a **conversational server** — an MCP that lets me, or Claude, or the voice
|
||||||
voice assistant it hangs off of) just *say* what happened. "He ate ninety at
|
assistant it hangs off of, just *say* what happened. "He ate ninety at eleven." It
|
||||||
eleven." It writes the same feeding record the app would, the same one the watch
|
writes the same feeding record the app would, the same one the watch would, the
|
||||||
would, the same one the swamp model syncs back out.
|
same one the swamp model syncs back out.
|
||||||
|
|
||||||
And there is **Home Assistant**, which is the layer that nags. At 10:30 it
|
And there's **Home Assistant**, the layer that nags. At 10:30 it reminds whoever's
|
||||||
reminds whoever is still vertical to prep the overnight bottle. At 11 it reminds
|
still upright to make the overnight bottle. At 11 it reminds us the vitamin D goes
|
||||||
us the vitamin D goes *in* that bottle — the vitamin D of at least three names.
|
in it. If nobody acknowledges, it asks again. It's a very polite robot with a
|
||||||
If nobody acknowledges it, it asks again. It is, functionally, a very polite
|
clipboard, and right now it has a better memory than either of us.
|
||||||
robot co-parent with a clipboard.
|
|
||||||
|
|
||||||
Five interfaces. One baby. Every single one of them a different door into the
|
None of these sit on a stock Baby Buddy, either — the one underneath isn't stock
|
||||||
same little REST API.
|
anymore. Somewhere in here I ended up bolting push notifications onto it via FCM,
|
||||||
|
adding per-record audit fields so I can see who logged what, soft-deleting timers
|
||||||
|
so a fat-fingered stop doesn't just vanish, and writing a handful of new reports.
|
||||||
|
"Track the baby" quietly became "re-platform the baby tracker," which is a
|
||||||
|
sentence I'd be embarrassed by if I'd slept.
|
||||||
|
|
||||||
## Why any of this
|
Five interfaces. One baby. Every one of them a different door into the same little
|
||||||
|
REST API.
|
||||||
|
|
||||||
You could read all of that as a man building toys instead of sleeping, and you
|
## Why though
|
||||||
would not be entirely wrong. But there is a real thing underneath it, and it is
|
|
||||||
the least technical thing in this whole post.
|
|
||||||
|
|
||||||
A newborn is a 24-hour operation run by two people who are each getting about
|
You could read all of this as a man building toys instead of sleeping and you
|
||||||
five hours of sleep in shifts. The single hardest part is not any one feeding.
|
wouldn't be all the way wrong. But there's a real thing underneath it, and it's
|
||||||
It's the *handoff* — the 1 a.m. moment where one of us goes horizontal and the
|
the least technical part of the post.
|
||||||
other picks up the watch, and the only way that works is if we are both looking
|
|
||||||
at the same set of facts. When did he last eat. How much. Is he due. Did the
|
|
||||||
vitamin D happen or did I dream that.
|
|
||||||
|
|
||||||
All of this tracking — the app, the watch, the voice, the nags, and now the
|
A newborn is a 24-hour operation run by two people getting maybe five hours each,
|
||||||
swamp — is not quantified-self vanity. It is a shared source of truth for two
|
in shifts. The hardest part isn't any single feeding. It's the handoff — the 1am
|
||||||
exhausted adults trying to hold one continuous story between them across a
|
moment where one of us goes horizontal and the other picks up the watch, and that
|
||||||
shift change, in the dark, without waking each other up to ask. The
|
only works if we're both looking at the same set of facts. When did he last eat.
|
||||||
instrumentation is the coordination layer. The baby is just where the data
|
How much. Is he due. Did the vitamin D happen or did I dream it.
|
||||||
happens to come from.
|
|
||||||
|
All the tracking — app, watch, voice, nags, swamp — isn't quantified-self stuff.
|
||||||
|
It's a shared source of truth for two very tired adults trying to hold one
|
||||||
|
continuous story between them across a shift change, in the dark, without waking
|
||||||
|
each other up to ask. The baby is just where the data comes from.
|
||||||
|
|
||||||
## The bit worth keeping
|
## The bit worth keeping
|
||||||
|
|
||||||
If there's one engineering lesson that survived contact with a real newborn, it's
|
If one engineering lesson survived contact with a real newborn, it's the same one
|
||||||
the same one from the certificate post, wearing a smaller hat: **data that claims
|
from the [certificate post]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}})
|
||||||
to be complete when it isn't is worse than no data.**
|
wearing a smaller hat: data that claims to be complete when it isn't is worse than
|
||||||
|
no data.
|
||||||
|
|
||||||
When I built the `sync` method, it capped each query at some number of records. A
|
When I wrote the `sync` method it capped each query at some number of records. A
|
||||||
sensible default, until you imagine a parent syncing a long window, quietly
|
sensible default — until you picture a parent syncing a long window, quietly
|
||||||
losing the oldest entries, and getting a report that looks whole. So the snapshot
|
dropping the oldest entries, and getting a summary that looks whole. So the
|
||||||
carries a `truncated` flag now — an honest "there was more than this" — and the
|
snapshot carries a `truncated` flag now, an honest "there was more than this," and
|
||||||
summary says so out loud instead of lying with a clean-looking table. A review
|
the summary says so out loud instead of lying with a tidy little table.
|
||||||
pass caught it before it shipped, which is the entire reason to have a review
|
|
||||||
pass.
|
|
||||||
|
|
||||||
The same instinct is why `delete-entry` swallows a 404 and calls it done. Two
|
Same instinct is why the timers soft-delete instead of actually deleting, and why
|
||||||
parents, two devices, one baby — someone *will* delete the same thing twice. The
|
stop-timer won't silently throw away a running one. Two parents, two devices, one
|
||||||
tool's job is to make the second delete boring, not to punish you for the racing
|
baby: someone is going to stop the wrong thing or delete the same thing twice. The
|
||||||
condition of being a family.
|
tool's job is to make that boring, not to punish you for the race condition of
|
||||||
|
being a family.
|
||||||
|
|
||||||
He's asleep right now. Twenty-two minutes in, per the timer on my wrist, which I
|
He's asleep right now — nineteen minutes in, per the timer I am absolutely going
|
||||||
will not be checking again, because that would wake the report, which would wake
|
to stop checking, because checking it wakes the report, which wakes me. The
|
||||||
me. The extension is [open source on the swamp
|
extension's open source on the swamp registry if you also run Baby Buddy and also
|
||||||
registry](https://github.com/NeilHanlon/swamp-babybuddy) if you also run Baby
|
cannot leave a working API alone.
|
||||||
Buddy and also cannot leave a working REST API alone.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
I do infrastructure and automation for a living through [my consulting
|
I point these same instincts at other people's infrastructure through
|
||||||
practice](https://shrugpw.com) — the kind of instrumentation that is nominally
|
[Shrug PW](https://shrugpw.com). The stakes are higher and the sleep is better.
|
||||||
for adults. The methodology, it turns out, is identical. There is just less
|
|
||||||
spit-up.
|
|
||||||
|
|||||||
@@ -1,132 +1,137 @@
|
|||||||
---
|
---
|
||||||
title: "The WiFi Certificate That Didn't Exist an Hour Ago"
|
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."
|
description: "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."
|
||||||
date: 2026-07-10T18:30:00-04:00
|
date: 2026-07-10T18:30:00-04:00
|
||||||
draft: true
|
draft: true
|
||||||
categories: ['infrastructure', 'automation']
|
categories: ['infrastructure', 'automation']
|
||||||
tags: ['freeipa', 'swamp', 'pki', '802.1x', 'eap-tls', 'radius', 'homelab', 'devops']
|
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
|
My laptop is on WiFi right now on a certificate that didn't exist an hour ago. I
|
||||||
hour ago. I generated the keypair, my own certificate authority signed it, the
|
made the keypair, my own CA signed it, the private key went into my secret store,
|
||||||
private key went straight into encrypted storage, and the machine walked onto
|
and the machine authenticated onto the 802.1X network and pulled a DHCP lease —
|
||||||
the 802.1X network and pulled a DHCP lease — all through a tool I had just
|
all through code I'd finished writing about twenty minutes earlier. I want to
|
||||||
finished writing. This is a short note on how that came together, and on the one
|
talk about the boring part that made it safe, because the boring part is the only
|
||||||
piece of engineering I think is actually worth sharing.
|
part I'd defend.
|
||||||
|
|
||||||
## The itch
|
## The itch
|
||||||
|
|
||||||
I run [FreeIPA](https://www.freeipa.org/) at home — my own Kerberos realm, LDAP
|
I run [FreeIPA](https://www.freeipa.org/) at home. Kerberos realm, LDAP, a CA —
|
||||||
directory, and certificate authority, the same identity stack a lot of companies
|
the same identity stack plenty of companies run, except mine is operated entirely
|
||||||
run in the large. It is a genuinely great piece of software. It is also, in my
|
by hand. `kinit`, the `ipa` CLI, a shell script when I'm feeling fancy. Every
|
||||||
setup, operated entirely by hand: `kinit`, the `ipa` CLI, a shell script when I
|
cert, every group, every bit of it is me typing commands and hoping I remembered
|
||||||
am feeling fancy. Every user certificate, every group, every bit of it is a
|
the flags. It works, and it's also exactly the sort of thing I'll fat-finger at
|
||||||
human typing commands and hoping they remembered the flags.
|
11pm.
|
||||||
|
|
||||||
I wanted it declarative. I wanted to describe an identity object and have
|
I wanted it declarative: describe an identity object, have something reconcile
|
||||||
something reconcile it, keep a versioned record of what happened, and let me
|
it, keep a record of what it did, and let me wire one piece of the homelab into
|
||||||
wire one piece of infrastructure into the next.
|
the next.
|
||||||
|
|
||||||
## What swamp is, briefly
|
## swamp, quickly
|
||||||
|
|
||||||
[swamp](https://github.com/swamp-club/swamp) models real resources as typed
|
[swamp](https://github.com/swamp-club/swamp) models real resources as typed
|
||||||
objects with methods — create, read, mutate — and captures every run as
|
objects with methods — create, read, mutate — and keeps every run as versioned
|
||||||
versioned data you can reference from other models. You extend it in TypeScript.
|
data other models can read. You write extensions in TypeScript. It's the good
|
||||||
Think of it as the good parts of infrastructure-as-code without pretending the
|
parts of infrastructure-as-code without the pretense that the world holds still.
|
||||||
world is a static graph. FreeIPA had no coverage in the registry, so this was
|
FreeIPA had nothing in the registry, so I got to start from scratch, which is my
|
||||||
greenfield.
|
favorite way to start.
|
||||||
|
|
||||||
## A family, not a monolith
|
## Not one model, a family
|
||||||
|
|
||||||
FreeIPA is a platform, not a service, so I split the work into a family of small
|
FreeIPA is a platform, not a service, so I split it into a few small packages
|
||||||
packages rather than one god-model. The first, `@shrug/freeipa/domain`, is
|
instead of one enormous one. `@shrug/freeipa/domain` is read-only: it logs into
|
||||||
read-only: it logs into the JSON-RPC API and snapshots the realm, the server
|
the JSON-RPC API and snapshots the realm, the server inventory, the replication
|
||||||
inventory, and the replication topology. "Understand the domain" before you
|
topology. Understand the domain before you touch it.
|
||||||
touch it.
|
|
||||||
|
|
||||||
Then the interesting ones, the packages that *write*: `user`, `group`, and
|
Then the ones that write: `user`, `group`, and `cert`. Certs got their own
|
||||||
`cert`. I split certificates into their own package on purpose — in FreeIPA a
|
package on purpose — in FreeIPA a certificate can belong to a user, a host, or a
|
||||||
certificate can be issued to a user, a host, or a service, so binding cert
|
service, so hanging cert issuance off the "user" model would have been wrong the
|
||||||
issuance to the "user" model would have been wrong the moment I wanted a device
|
second I wanted a device cert. (I wanted a device cert.)
|
||||||
cert.
|
|
||||||
|
|
||||||
I built the three write packages in parallel — three agents, three repositories,
|
I built the three write packages at the same time, in parallel: three agents,
|
||||||
each mirroring the structure of the read-only one and each carrying a
|
each in its own git worktree, each cloning the shape of the read-only package and
|
||||||
byte-for-byte identical "write-kernel." That shared kernel is where the real
|
each carrying an identical copy of the same "write-kernel."
|
||||||
thinking went.
|
|
||||||
|
|
||||||
## The part worth sharing: how do you safely persist a mutation?
|
### What the write-kernel actually is
|
||||||
|
|
||||||
Here is a question that sounds trivial and is not: when a mutating operation
|
That last bit needs unpacking, because "byte-for-byte identical write-kernel" is
|
||||||
runs, *when* do you write down what happened?
|
the kind of phrase that sounds like it means something and might not. It's not a
|
||||||
|
shared library — each package vendors its own copy. It's a small module that
|
||||||
|
every write goes through: it takes the mutation you want, runs it, and decides
|
||||||
|
what gets persisted and when. Same code, same rules, in all three packages, so
|
||||||
|
"how do we write things down" gets answered once and copied, not reinvented three
|
||||||
|
times slightly differently. Why it's worth copying is the next section.
|
||||||
|
|
||||||
The default guidance — swamp's, and honestly most people's instinct — is **throw
|
## The actual point: when do you write it down?
|
||||||
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.
|
Here's a question that sounds trivial and isn't: when a mutating operation runs,
|
||||||
|
*when* do you record what happened?
|
||||||
|
|
||||||
**Irreplaceable material.** When I issue a certificate, the model generates a
|
swamp's default guidance — and honestly most people's reflex — is throw before
|
||||||
fresh private key, sends the signing request, and the CA signs it. The
|
you write. If the op fails, persist nothing, because a stored record claiming a
|
||||||
certificate now *exists*. If any step after that throws and I had followed "throw
|
success the world never delivered is a lie, and downstream models will read that
|
||||||
before writing," I would lose the only private key that matches a certificate
|
lie and act on it. That's correct, and I kept it. But it quietly assumes every
|
||||||
that is already real. Here the safe-sounding rule is actively destructive. The
|
kind of "what happened" wants to be written at the same instant, and two of them
|
||||||
right principle is the opposite: persist irreplaceable state the instant it
|
don't.
|
||||||
becomes real, before anything else can fail.
|
|
||||||
|
|
||||||
**Partial failure.** One of the operations creates a steering group as *both* a
|
The first is **irreplaceable material.** When I issue a cert, the model generates
|
||||||
user-group and a host-group — two independent calls. If the first lands and the
|
a fresh private key, sends the signing request, and the CA signs it. The cert
|
||||||
second errors, "it failed" is true but useless; what you need to know is which
|
exists now. If something after that throws and I'd followed "throw before
|
||||||
half exists so you can reconcile it.
|
writing," I'd have thrown away the only private key that matches a certificate
|
||||||
|
that's already, really, out in the world. So that one flips: write the
|
||||||
|
irreplaceable thing the moment it's real, before anything else gets a chance to
|
||||||
|
fail.
|
||||||
|
|
||||||
So the kernel follows a three-way rule instead of a slogan:
|
The second is **partial failure.** One operation creates a steering group as both
|
||||||
|
a user-group and a host-group — two separate calls. If the first lands and the
|
||||||
|
second blows up, "it failed" is technically true and completely useless. What I
|
||||||
|
need to know is which half exists, so I can go fix the other one.
|
||||||
|
|
||||||
|
So the kernel follows three rules instead of a slogan:
|
||||||
|
|
||||||
1. **State** — the object itself — is written only on success. A failed mutation
|
1. **State** — the object itself — is written only on success. A failed mutation
|
||||||
never publishes misleading state. (The original rule, intact.)
|
never publishes state that says otherwise. (The original rule, untouched.)
|
||||||
2. **Irreplaceable material** — a generated key, a signed cert — is persisted the
|
2. **Irreplaceable material** — a generated key, a signed cert — is written the
|
||||||
moment it is real, before any later throw can eat it.
|
moment it's real, before any later throw can eat it.
|
||||||
3. An **audit record** is written on *both* paths, success and failure. It is
|
3. An **audit record** is written on *both* paths, success and failure. It's
|
||||||
telemetry, not state; when it says `success: false` it is telling the truth,
|
telemetry, not state; when it says `success: false` it's telling the truth, so
|
||||||
so persisting it on failure violates nothing.
|
writing it on failure isn't lying about anything.
|
||||||
|
|
||||||
The slogan is right until the domain proves it wrong in a specific, nameable
|
That's the whole idea. "Only write on success" is a fine default right up until
|
||||||
way. Then you deviate on purpose, and you write down why.
|
the domain hands you a case where it's actively wrong — and then you break it on
|
||||||
|
purpose and leave a comment saying why.
|
||||||
|
|
||||||
## The payoff
|
## The payoff, which is deliberately boring
|
||||||
|
|
||||||
With that in place the actual automation is almost boring, which is the goal.
|
With that in place the rest is unglamorous, which was the goal. One method makes
|
||||||
One method idempotently ensures the VLAN-steering group exists as both a
|
sure the VLAN-steering group exists as both group types and shrugs off "already
|
||||||
user-group and a host-group, swallowing "already exists" so re-runs are safe.
|
exists" so I can re-run it. Another generates an RSA keypair and a PKCS#10 request
|
||||||
Another generates an RSA keypair and a PKCS#10 request in-process, submits it to
|
in process, hands it to the CA, and stashes the resulting private key encrypted in
|
||||||
the CA, and stores the resulting private key **encrypted in my secret store** —
|
my secret store — never in plaintext, never in the model's own data.
|
||||||
never in plaintext, never in the model's own data.
|
|
||||||
|
|
||||||
Then the last mile, by hand because it should be: pull the signed certificate
|
Then the last mile, by hand, because it should be: pull the signed cert and key
|
||||||
and the key out to files, point NetworkManager's EAP-TLS profile at them, and
|
out to files, point NetworkManager's EAP-TLS profile at them, bring the interface
|
||||||
bring the interface up. The RADIUS server validated the certificate against my
|
up. RADIUS checked the cert against my CA, said yes, dropped me on a VLAN, and
|
||||||
CA, said yes, dropped me on a VLAN, and DHCP did the rest. I was online — on an
|
DHCP did the rest. Online — on an SSID with a potato in the name, because it's my
|
||||||
SSID with a potato in the name, because this is *my* network and I make the
|
network and I get to name the SSID.
|
||||||
rules.
|
|
||||||
|
|
||||||
A certificate that did not exist when I started the afternoon, issued by
|
A certificate that didn't exist when I sat down, issued by infrastructure I
|
||||||
infrastructure I automated the same afternoon, carrying a laptop onto the
|
automated the same afternoon, carrying my laptop onto the network. Loop closed.
|
||||||
network. That is the whole loop, closed.
|
|
||||||
|
|
||||||
## Why bother
|
## Why bother
|
||||||
|
|
||||||
I could have typed the `ipa` commands. I have, many times. But automating a thing
|
I could have typed the `ipa` commands. I have, a hundred times. But automating
|
||||||
is how you are forced to actually understand it — every flag you were cargo-culting,
|
something is how you're forced to actually understand it — every flag you'd been
|
||||||
every failure mode you were getting away with ignoring. The write-safety pattern
|
copy-pasting on faith, every failure mode you'd been quietly getting away with.
|
||||||
above did not come from wanting clever code; it came from asking "what happens if
|
The write-safety stuff up top didn't come from wanting clever code. It came from
|
||||||
this throws *here*" until the answer stopped being "you lose your private key."
|
asking "what happens if this throws *here*" over and over until the answer
|
||||||
|
stopped being "you lose the private key."
|
||||||
|
|
||||||
The packages are open source on the swamp registry. If you run FreeIPA and want
|
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
|
to stop hand-driving it — or you just like arguing about when to persist a
|
||||||
a failure — come find me.
|
failure — come find me.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
I do this kind of infrastructure and automation work through
|
This is what I do for money too, through [Shrug PW](https://shrugpw.com): bigger
|
||||||
[my consulting practice](https://shrugpw.com), and I am around the usual places
|
networks, fewer potatoes.
|
||||||
on the internet.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user