entry-16
Publishing homelab evidence without leaking it
A repeatable, OCR-assisted, multi-pass workflow for redacting homelab screenshots before they go into public repos: locate the evidence, detect IPs, MACs and personal identifiers with tesseract, verify by hand on the redacted output, then embed the cleared images in validation docs.
revisions rev. 1 first set 2026-10-23 last revised 2026-10-23 8 min read
Update (2026-10-05): The credential rotation described below was completed on this date, with failover and failback checked afterward.
The homelab has two audiences for the same work. A private repository is where the real evidence lives: raw output, real addresses, real hostnames, the whole unedited trail. The public repos are where the write-ups live, and they have to show enough proof to be worth reading without handing over a map of the network.
The gap between the two is mostly screenshots. A terminal capture looks harmless and is not. A prompt can carry a workstation hostname, a configuration diff can carry a credential, a DHCP table can carry MAC addresses, a certificate log carries an ACME order URL with the account ID baked into it. None of that is obvious in a thumbnail, and all of it is a copy of the private repo sitting in a public one.
This is the workflow I ended up with, after doing it by hand first and getting it wrong.
Why the manual pass wasn’t enough
The first pass was manual: open each image, guess the pixel coordinates of the sensitive text, draw a black rectangle with PIL, move on. It does not hold up at scale.
The failure mode is that a rectangle is a coordinate guess. Even with generous margins, inspecting the result still turned up fragments of a MAC or IP at the edge of a box. Coordinates picked by eye do not survive a re-export, a different crop, or the same text landing one line lower in the next screenshot. Worse, you cannot tell from the source image where the text actually is, only where you think it is.
The fix was to stop guessing coordinates and read the text instead. OCR gives you word-level bounding boxes, so the box gets drawn where the text actually is.
Step 1: locate the screenshots
The screenshot set is the evidence, and the set is not always sitting in the repo when you go looking for it. The Phase 3 VPS evidence was captured during the deploy session but never carried into the repo; it sat outside until a later pass went back and located all of it. So the first step is not redaction, it’s finding every image that belongs to the phase, including the ones that were taken but not committed.
The layout rule that makes the rest safe: originals stay where they are,
in the repo’s screenshots/ tree, as the private evidence trail. Cleared
copies go to a parallel screenshots-redacted/ tree with the same
filenames and structure. Only the parallel tree is ever linked from
public docs. Nothing is ever edited in place, so a bad pass can be
thrown away and redone without losing the source.
Step 2: OCR-assisted detection
The tool is small: tesseract for recognition, PIL for drawing. It
walks every PNG in the source tree, asks for word-level boxes, and
matches each word against two things: an IPv4 regex and a MAC regex, and
a configurable keyword list of personal identifiers to redact. A hit
gets a padded black box over its box. The result is written to the
mirrored tree; the source is only ever opened for reading.
The detection core, simplified:
IP_RE = re.compile(r"\d{1,3}(?:\.\d{1,3}){3}")
MAC_RE = re.compile(r"[0-9a-fA-F]{2}(?:[:.-][0-9a-fA-F]{2}){5}")
KEYWORDS = {"<workstation-hostname>", "<username-one>", "<username-two>"}
for word, x, y, w, h in recognized_words(image):
if IP_RE.search(word) or MAC_RE.search(word) or word.lower() in KEYWORDS:
draw.rectangle([x - pad, y - pad, x + w + pad, y + h + pad], fill="black")
Both halves matter. The regexes catch the structured things, addresses and hardware IDs. The keyword list catches the things regex cannot know are sensitive: a Windows workstation hostname, or usernames that recur in prompts and file paths across dozens of unrelated terminal screenshots. That list is per-repo and configurable, because the identifiers that leak are mine, not a fixed pattern.
The first full sweep, across three public repos, looked like this:
| Repo | Screenshots audited | Needed redaction |
|---|---|---|
| dns | 121 | |
| vps-lab | 20 | |
| netlab | 16 | |
| Total | 157 | 92 |
What it turned up was not abstract. Real device MAC addresses throughout. Real private LAN and Tailscale addresses. My Windows hostname and two usernames recurring across dozens of terminal captures in all three repos. The sweep also found an exposed VRRP authentication credential. I removed that exposure from the public evidence, then rotated the credential on 2026-10-05 and validated failover and failback. Cleaning an image alone would not have revoked the exposed value.
The VPS repo added one more category the regex did not know about: the Let’s Encrypt ACME order URLs in the Caddy access log. They look like noise, and they contain the ACME account ID. That is the kind of thing OCR can surface but cannot classify. It needs a human.
Step 3: layered manual verification
The single most important thing I learned: an automated sweep is evidence you checked, not proof nothing was missed. It has to be treated as a first pass, not a verdict.
Two concrete misses made the case.
The first was a machine error. OCR misread a MAC address on a topology diagram and skipped it entirely. The detector was working as designed; the text was just read wrong. It was caught by a manual visual pass over the actual redacted output, not by the tool.
The second was self-inflicted. While patching that miss, the fix script was run against the original unredacted source instead of the already-OCR-redacted output. Because the script derives output from its input, that silently reverted three prior redactions on the same image: a real MAC and the real hostname went from covered back to exposed. It was caught on the next visual check, but only because that check happened.
That second one generalizes past screenshots. Layered edits compose; re-deriving from scratch does not. When you patch a derived artifact, patch the derived artifact. Build the fix on the most recently redacted version, never on the original source, unless regenerating from source is the explicit intent.
So the pass is layered on purpose:
- Run OCR, then open the redacted output and look at it by hand. This is what catches OCR’s misreads.
- On top of that, review the semantic cases the regex cannot judge: is this URL an account identifier, is that a real email, does this admin panel show something the pattern list has never heard of. This is where the ACME order URLs, validation remotes, and account email get marked.
- If anything gets patched, patch the redacted output and then look again. Every targeted fix earns another visual check, because the fix itself is the risk.
Step 4: embed the cleared screenshots
The reason any of this exists is that the public repos use screenshots as validation evidence. So the last step is wiring the cleared set into the validation doc, one image per evidence item, each with a caption that says what it proves.
The Phase 3 VPS write-up is the pattern. Nine evidence items, images linked from the redacted tree, covering the container state, certificate issuance, the HTTP redirect, the certificate chain, security headers, the JSON access log, and the backend port probes. Item 06 was confirmed not applicable after the phase retargeted, and a bonus item was added for the Cloudflare zone that turned out to be the real blocker. The doc also carries a redaction table, so the removal is stated instead of implied:
| Sensitive item | Handling |
|---|---|
| Public IPv4 / IPv6 address | Redacted |
| Tailscale IP | Redacted |
| Admin workstation hostname | Redacted |
| ACME / account email | Redacted |
| ACME order URLs (contain the account ID) | Redacted |
| Let’s Encrypt validation remotes | Redacted by default |
| SSH host fingerprints | Redacted where visible |
| Provider account data | Excluded |
The commit only happened after my own visual pass over the final images, as a docs-only PR. That ordering is the point: the automated pass proposes, a human disposes.
Verify across repos, don’t assume
The original mistake was assuming that because the HA-DNS repo had been swept, the other public repos were fine by extension. They were not. The only reason the other two got checked is that I asked whether every relevant repo had actually been covered, and the honest answer was no.
Auditing all three for real surfaced a DHCP MAC/IP table and a final-topology exposure in the managed-network repo that were not visible from the HA-DNS work at all. Each repo got its own PR, all docs-only, each merged only after CI passed under branch protection. None of that would have happened if the assumption had been left standing.
Closing note
Three things worth repeating.
Automate the detection, not the judgment. OCR tells you where the text is; a human decides what a string means. A clean tool run is not the same as a clean set, and the miss that got through here was a machine misread, not a human oversight.
Patch the derived artifact. A fix built from the original source is a regression waiting to happen, and it can quietly reopen exactly the things you already closed.
Keep the originals. Redaction is additive: the source stays in the private tree, the cleared copies go to a parallel public tree, and nothing is edited in place. That is what makes it safe to iterate, throw away a bad pass, and still have the evidence when you are done. The public write-ups are the point; the redaction workflow is the tax you pay to publish them without handing over the network.
Related posts: The DNS panel that wasn’t authoritative, Cutting over from mesh Wi-Fi to a managed network, live.