From 736a10eb32a5a8983e131c2a0ff0d4638d730d18 Mon Sep 17 00:00:00 2001 From: Neil Hanlon Date: Sat, 11 Jul 2026 22:11:20 -0400 Subject: [PATCH] Rewrite both AI drafts in Neil's voice (still draft) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- content/posts/i-put-my-son-in-a-swamp.md | 198 +++++++++--------- ...-wifi-cert-that-didnt-exist-an-hour-ago.md | 187 +++++++++-------- 2 files changed, 194 insertions(+), 191 deletions(-) diff --git a/content/posts/i-put-my-son-in-a-swamp.md b/content/posts/i-put-my-son-in-a-swamp.md index 98b21aa..156e286 100644 --- a/content/posts/i-put-my-son-in-a-swamp.md +++ b/content/posts/i-put-my-son-in-a-swamp.md @@ -1,6 +1,6 @@ --- 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 draft: true 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 -days of his life into a versioned snapshot, fifteen reports that summarize how he -slept and ate and — I want to be clear that I did think about this — a -`delete-entry` method that is careful to be idempotent, because deleting the same -diaper twice should not be an error. +days of his life into a versioned snapshot, a stack of reports that summarize how +he slept and ate, and — I did think about this — a delete that's careful to be +idempotent, because deleting the same diaper twice shouldn't be an error. -I did not set out to put a baby in a swamp. It just followed, the way these -things do, from already having put everything else in one. +I did not set out to put a baby in a swamp. It followed, the way these things do, +from having already put everything else in one. ## What "in a swamp" means [swamp](https://github.com/swamp-club/swamp) models real resources as typed -objects with methods — read them, mutate them — and records every run as -versioned data other models can reference. You write extensions in TypeScript. I -used it [last week to automate my home certificate -authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}), and the -whole time I kept thinking about the thing I actually spend all night touching, -which is not a certificate. +objects with methods, and records every run as versioned data other models can +read. You write extensions in TypeScript. I used it [last week to automate my home +certificate authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}), +and the whole time I kept thinking about the thing I actually spend all night +dealing with, which is not a certificate. 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, -temperature, meds. It has a clean REST API. And a REST API, to a certain kind of -tired brain at 2 a.m., is an invitation. +self-hosted app for tracking a newborn's feedings, diapers, sleep, pumping, meds. +It has a clean REST API. A clean REST API, at 2am, to a certain kind of brain, is +a dare. 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. Log a feeding, delete a mis-logged one, patch a bad timestamp. Then -ask it questions. +snapshot and writes new entries back, with a stack of reports that run over the +snapshot. Log a feeding, fix a mis-logged one, patch a bad timestamp, then ask it +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 -their own, quietly insane. Ninety-nine feedings. Sixty-five separate sleep -sessions. Forty-four doses of medication. His longest unbroken stretch of sleep -was four hours and forty-eight minutes, which I know to the minute because there -is a report called `sleep-longest-stretch` and I run it the way other people -check the weather. +Summarizing a week of a newborn produces numbers that are, on their own, a little +unhinged. In the last seven days: ninety-six feedings. Sixty-five separate sleep +sessions. His longest unbroken stretch was four hours and forty-six minutes, +which I know to the minute because there's a report called `sleep-longest-stretch` +and I run it the way other people check the weather. -But the report I did not expect to matter was the medication one. I built it to -answer "are we dosing the vitamin D on schedule," and instead it answered a -question I had not asked, which is "how many different ways has Neil spelled -vitamin D at midnight." The answer is three. `Vitamin D`, `Vitamin D Drops`, and -`Vitamin D drops` are, as far as the database is concerned, three unrelated -substances, dosed on three overlapping schedules, by one man who was very tired. +The one that got me was the medication report. I built it to answer "are we giving +the vitamin D on schedule," and it answered a question I hadn't asked, which is +"when does Neil actually remember the vitamin D." Not on schedule, it turns out. +It's a little cluster of timestamps at midnight, 2am, 3am — whenever I surfaced +enough to think of it. The schedule in my head and the schedule in the database +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 -the dashboard shows you your own sloppiness back. +That's the actual value of instrumenting anything: some night the dashboard hands +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 -affection for the person who did it: the swamp extension is the *newest* way I -have to track this child, not the only one, and not by a wide margin. +Here's the genuinely stupid part, and I say that with affection for the man who +did it: the swamp extension is the *newest* way I have to track this kid, not the +only one, and not by a wide margin. -There is 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 -whole night's log. Siri intents, so "log a diaper" is a sentence you say into the -air with both hands full, which is the only state your hands are ever in. Live -Activities, so a running feeding timer ticks on the lock screen next to the -elapsed time you are trying not to look at. +There's a **mobile app** — a Capacitor wrapper around Baby Buddy with the sharp +edges filed down. Biometric lock, so a phone left on the couch doesn't show the +whole night's log. Log-a-diaper you can say out loud, because your hands are full, +because your hands are always full. A running feed timer on the lock screen next +to the elapsed time you're trying not to look at. iOS and Android, because of +course iOS and Android. -There is a **Pebble watchapp**, because I have a Pebble again and it turns out the -best interface for logging a feed at 3 a.m. is a physical button on your wrist -that does not emit a single photon more than necessary. Start a timer, stop it, -pick the amount, done — without the wake-up-your-whole-nervous-system brightness -of a phone. There is a Pebble smart *ring* on order for the same reason. I am not -going to defend the ring. The ring defends itself. +There's a **Pebble watchapp**, because the best possible interface for logging a +feed at 3am is a physical button on your wrist that emits about four photons. +Start a timer, stop it, pick the amount, done, without the wake-your-whole-nervous- +system glare of a phone. I'll admit the watchapp is currently ahead of my +hardware — I don't have the watch yet. There's a ring interface too. I don't have +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 -voice assistant it hangs off of) just *say* what happened. "He ate ninety at -eleven." It writes the same feeding record the app would, the same one the watch -would, the same one the swamp model syncs back out. +There's a **conversational server** — an MCP that lets me, or Claude, or the voice +assistant it hangs off of, just *say* what happened. "He ate ninety at eleven." It +writes the same feeding record the app would, the same one the watch would, the +same one the swamp model syncs back out. -And there is **Home Assistant**, which is the layer that nags. At 10:30 it -reminds whoever is still vertical to prep the overnight bottle. At 11 it reminds -us the vitamin D goes *in* that bottle — the vitamin D of at least three names. -If nobody acknowledges it, it asks again. It is, functionally, a very polite -robot co-parent with a clipboard. +And there's **Home Assistant**, the layer that nags. At 10:30 it reminds whoever's +still upright to make the overnight bottle. At 11 it reminds us the vitamin D goes +in it. If nobody acknowledges, it asks again. It's a very polite robot with a +clipboard, and right now it has a better memory than either of us. -Five interfaces. One baby. Every single one of them a different door into the -same little REST API. +None of these sit on a stock Baby Buddy, either — the one underneath isn't stock +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 -would not be entirely wrong. But there is a real thing underneath it, and it is -the least technical thing in this whole post. +## Why though -A newborn is a 24-hour operation run by two people who are each getting about -five hours of sleep in shifts. The single hardest part is not any one feeding. -It's the *handoff* — the 1 a.m. moment where one of us goes horizontal and the -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. +You could read all of this as a man building toys instead of sleeping and you +wouldn't be all the way wrong. But there's a real thing underneath it, and it's +the least technical part of the post. -All of this tracking — the app, the watch, the voice, the nags, and now the -swamp — is not quantified-self vanity. It is a shared source of truth for two -exhausted adults trying to hold one continuous story between them across a -shift change, in the dark, without waking each other up to ask. The -instrumentation is the coordination layer. The baby is just where the data -happens to come from. +A newborn is a 24-hour operation run by two people getting maybe five hours each, +in shifts. The hardest part isn't any single feeding. It's the handoff — the 1am +moment where one of us goes horizontal and the other picks up the watch, and that +only works if we're 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 it. + +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 -If there's one engineering lesson that survived contact with a real newborn, it's -the same one from the certificate post, wearing a smaller hat: **data that claims -to be complete when it isn't is worse than no data.** +If one engineering lesson survived contact with a real newborn, it's the same one +from the [certificate post]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}) +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 -sensible default, until you imagine a parent syncing a long window, quietly -losing the oldest entries, and getting a report that looks whole. So the snapshot -carries a `truncated` flag now — an honest "there was more than this" — and the -summary says so out loud instead of lying with a clean-looking table. A review -pass caught it before it shipped, which is the entire reason to have a review -pass. +When I wrote the `sync` method it capped each query at some number of records. A +sensible default — until you picture a parent syncing a long window, quietly +dropping the oldest entries, and getting a summary that looks whole. So the +snapshot carries a `truncated` flag now, an honest "there was more than this," and +the summary says so out loud instead of lying with a tidy little table. -The same instinct is why `delete-entry` swallows a 404 and calls it done. Two -parents, two devices, one baby — someone *will* delete the same thing twice. The -tool's job is to make the second delete boring, not to punish you for the racing -condition of being a family. +Same instinct is why the timers soft-delete instead of actually deleting, and why +stop-timer won't silently throw away a running one. Two parents, two devices, one +baby: someone is going to stop the wrong thing or delete the same thing twice. The +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 -will not be checking again, because that would wake the report, which would wake -me. The extension is [open source on the swamp -registry](https://github.com/NeilHanlon/swamp-babybuddy) if you also run Baby -Buddy and also cannot leave a working REST API alone. +He's asleep right now — nineteen minutes in, per the timer I am absolutely going +to stop checking, because checking it wakes the report, which wakes me. The +extension's open source on the swamp registry if you also run Baby Buddy and also +cannot leave a working API alone. --- -I do infrastructure and automation for a living through [my consulting -practice](https://shrugpw.com) — the kind of instrumentation that is nominally -for adults. The methodology, it turns out, is identical. There is just less -spit-up. +I point these same instincts at other people's infrastructure through +[Shrug PW](https://shrugpw.com). The stakes are higher and the sleep is better. diff --git a/content/posts/the-wifi-cert-that-didnt-exist-an-hour-ago.md b/content/posts/the-wifi-cert-that-didnt-exist-an-hour-ago.md index dba5466..718bcba 100644 --- a/content/posts/the-wifi-cert-that-didnt-exist-an-hour-ago.md +++ b/content/posts/the-wifi-cert-that-didnt-exist-an-hour-ago.md @@ -1,132 +1,137 @@ --- 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 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. +My laptop is on WiFi right now on a certificate that didn't exist an hour ago. I +made the keypair, my own CA signed it, the private key went into my secret store, +and the machine authenticated onto the 802.1X network and pulled a DHCP lease — +all through code I'd finished writing about twenty minutes earlier. I want to +talk about the boring part that made it safe, because the boring part is the only +part I'd defend. ## 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 run [FreeIPA](https://www.freeipa.org/) at home. Kerberos realm, LDAP, a CA — +the same identity stack plenty of companies run, except mine is operated entirely +by hand. `kinit`, the `ipa` CLI, a shell script when I'm feeling fancy. Every +cert, every group, every bit of it is me typing commands and hoping I remembered +the flags. It works, and it's also exactly the sort of thing I'll fat-finger at +11pm. -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. +I wanted it declarative: describe an identity object, have something reconcile +it, keep a record of what it did, and let me wire one piece of the homelab into +the next. -## What swamp is, briefly +## swamp, quickly [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. +objects with methods — create, read, mutate — and keeps every run as versioned +data other models can read. You write extensions in TypeScript. It's the good +parts of infrastructure-as-code without the pretense that the world holds still. +FreeIPA had nothing in the registry, so I got to start from scratch, which is my +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 -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. +FreeIPA is a platform, not a service, so I split it into a few small packages +instead of one enormous one. `@shrug/freeipa/domain` is read-only: it logs into +the JSON-RPC API and snapshots the realm, the server inventory, 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. +Then the ones that write: `user`, `group`, and `cert`. Certs got their own +package on purpose — in FreeIPA a certificate can belong to a user, a host, or a +service, so hanging cert issuance off the "user" model would have been wrong the +second I wanted a device cert. (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. +I built the three write packages at the same time, in parallel: three agents, +each in its own git worktree, each cloning the shape of the read-only package and +each carrying an identical copy of the same "write-kernel." -## 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 -runs, *when* do you write down what happened? +That last bit needs unpacking, because "byte-for-byte identical write-kernel" is +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 -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. +## The actual point: when do you write it down? -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 -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. +swamp's default guidance — and honestly most people's reflex — is throw before +you write. If the op fails, persist nothing, because a stored record claiming a +success the world never delivered is a lie, and downstream models will read that +lie and act on it. That's correct, and I kept it. But it quietly assumes every +kind of "what happened" wants to be written at the same instant, and two of them +don't. -**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. +The first is **irreplaceable material.** When I issue a cert, the model generates +a fresh private key, sends the signing request, and the CA signs it. The cert +exists now. If something after that throws and I'd followed "throw before +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 - 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. + never publishes state that says otherwise. (The original rule, untouched.) +2. **Irreplaceable material** — a generated key, a signed cert — is written the + moment it's real, before any later throw can eat it. +3. An **audit record** is written on *both* paths, success and failure. It's + telemetry, not state; when it says `success: false` it's telling the truth, so + writing it on failure isn't lying about anything. -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. +That's the whole idea. "Only write on success" is a fine default right up until +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. -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. +With that in place the rest is unglamorous, which was the goal. One method makes +sure the VLAN-steering group exists as both group types and shrugs off "already +exists" so I can re-run it. Another generates an RSA keypair and a PKCS#10 request +in process, hands it to the CA, and stashes 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. +Then the last mile, by hand, because it should be: pull the signed cert and key +out to files, point NetworkManager's EAP-TLS profile at them, bring the interface +up. RADIUS checked the cert against my CA, said yes, dropped me on a VLAN, and +DHCP did the rest. Online — on an SSID with a potato in the name, because it's my +network and I get to name the SSID. -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. +A certificate that didn't exist when I sat down, issued by infrastructure I +automated the same afternoon, carrying my laptop onto the network. 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." +I could have typed the `ipa` commands. I have, a hundred times. But automating +something is how you're forced to actually understand it — every flag you'd been +copy-pasting on faith, every failure mode you'd been quietly getting away with. +The write-safety stuff up top didn't come from wanting clever code. It came from +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 -to stop operating it by hand — or if you just like arguing about when to persist -a failure — come find me. +to stop hand-driving it — or 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. +This is what I do for money too, through [Shrug PW](https://shrugpw.com): bigger +networks, fewer potatoes.