A physical release gate for my own infrastructure. Data on my NAS lives in an encrypted vault that only opens when I physically touch a YubiKey plugged into the machine — and a desktop security console shows me every request: who asked, from where, and when.
The problem. A NAS on your network will hand its files to anything that presents the right credentials — malware included. Credentials can be stolen, sessions can be hijacked, and nothing about a network protocol proves a person is present and consenting. The same gap exists one layer up: if you distribute demo builds of your software, a public download link gives you no idea who is taking what.
The solution. Put the data behind hardware. Deadbolt keeps sensitive files in a LUKS2-encrypted container whose key is derived from a YubiKey challenge–response that refuses to answer until the key's touch sensor is physically pressed. A small daemon on the NAS owns the state machine; a desktop console on my PC surfaces every unlock request the moment it arrives — with the requester's name and source address — and makes the "touch now" window unmissable. The same request → notify → approve pattern extends to my website: visitors ask for a demo build by name, my phone pings, and nothing downloads until I approve it.
The value. Data at rest is ciphertext until a human touches hardware. Every release of data — a file over SMB, a demo build over HTTPS — is a deliberate, attributable, logged act. A leaked token can only ask; it shows up in the audit trail either way.
stack: Python (stdlib-only daemon) · PySide6 console · LUKS2 + dm-crypt · YubiKey HMAC-SHA1 challenge–response · Samba · Cloudflare Pages Functions + KV + R2 · ntfy
The console during a live unlock — captured against the daemon's mock backend, which exists precisely so the whole UX can be exercised without hardware.
Built and deployed onto a stock UGREEN DXP2800 NAS — no OS wipe, no vendor encryption, everything brought along.
UGOS is Debian underneath, but its package state is broken upstream and firmware updates can revert changes. Deadbolt ships as a single container file inside a normal share, a stdlib-only daemon, and a systemd unit — nothing invasive, everything re-installable.
A mock backend simulates the whole flow on the PC, so the console, state machine, tray alerts and audit trail were finished and demoable before the first byte was encrypted. The real backend swaps in LUKS2 + a touch-required YubiKey slot behind the same API.
Kernel dm-crypt verified, dependencies installed against a broken apt by computing the exact package closure, a 500 GB vault created with a touch-derived key plus an offline recovery passphrase, and the decrypted mount wired to a normal SMB share.
With a Windows Explorer window holding the share open, the unmount failed quietly and the daemon still reported the vault sealed. Exactly the lie the project exists to prevent — caught on the first live run, because the verification ran against the real thing.
The seal path now force-closes open SMB handles, verifies against the kernel's own mount table, and refuses to claim LOCKED without proof. Three independent review lenses went after the fix before it shipped; the failure now reports honestly and retries.
Demo-build requests go through Cloudflare Pages Functions with KV state and private R2 storage: a visitor asks by name, my phone pings, and the download only exists after I approve it — time-boxed and download-capped.
Every claim above was exercised against the real system, not a slide.
| Claim | How it was verified | Result |
|---|---|---|
| Touch is the gate | unlock request → key blinks → touch → decrypted and mounted; no touch → sealed at 15 s | verified live |
| Files exist only while open | write over SMB → seal → share lists nothing; kernel mount table + mapper checked | verified live |
| A seal claim is proven, not assumed | open-handle EBUSY scenario re-run after the fix; LOCKED is never claimed without /proc/mounts proof | verified live |
| State machine holds under abuse | 10-check FSM smoke suite: idempotent re-lock, lock-during-unlock refused (409), timeout revert | 10 / 10 |
| Web approval flow end-to-end | 82-assertion API suite + rate-cap boundary test + console-to-download finale (real console × local Cloudflare runtime, pre-launch) | all green |
The decisions that made it trustworthy.
The first live seal attempt caught a real bug — a vault that claimed to be sealed while it wasn't — because verification ran against the real system. That's the point of building this way: the proof and the product are the same artifact.