Project
A self-hosted credential vault built on KeePassXC, synced across desktop, NAS, and Android via Syncthing over Tailscale. No third-party cloud in the sync path. Automated audit tooling enforces that every secret in every project repo has a corresponding vault entry.
A credential management system built from commodity open-source components: KeePassXC on Windows desktop and KeePassDX on Android as the vault clients; Syncthing running in Docker on a UGREEN NAS as the always-on sync hub; Tailscale as the VPN layer for secure out-of-home sync. The vault file is a standard .kdbx (encrypted with AES-256) protected by a passphrase and a key file. The key file lives only on endpoints, not on the NAS. An attacker who steals the .kdbx from the NAS Docker volume still needs the key file from a separate device.
The vault is deliberately framed as a cold-storage recovery layer, not a daily driver. Browser autofill keeps doing what it does. The vault holds the credentials worth archiving: recovery codes, automation service keys, the credentials that unlock other credentials. It is opened a few times a month, not dozens of times a day. That framing kept the design simple and the friction low.
The trigger was straightforward. I was running several personal infrastructure projects simultaneously (a public portfolio site, automation pipelines, a NAS-based home lab) and the credential surface was growing: API keys in .env files, webhook URLs in config files, service accounts in Windows Credential Manager, and some things only in my head. Before any of that grew further, I wanted a single canonical record.
Three framing options were evaluated. Framing 1: cold-storage recovery layer on top of the existing browser autofill setup. Framing 2: daily-driver replacement where the vault becomes the primary store. Framing 3: a sync layer maintained in parallel with autofill. Framing 3 is a trap; the maintenance tax never gets paid and the vault becomes stale within months. Framing 2 creates daily friction for a use case that doesn't need it. Framing 1 delivers the actual value (canonical record, audit surface, recovery safety net) without requiring a behavioral change to how the browser handles everyday passwords.
The commercial alternatives were all evaluated and rejected with explicit reasoning. Vaultwarden requires running a 24/7 service for a vault opened once a month. Proton Pass and 1Password both introduce a third-party trust dependency specifically for the credentials worth protecting. The self-hosted .kdbx approach trades away polished UX for full control and zero vendor dependency. That trade is correct for this use case.
Three-device sync cluster with a NAS as the always-on hub. All sync traffic flows over Tailscale; there is no third-party cloud relay.
KeePassXC on Windows. Primary write surface. Vault file at a local path; SyncTrayzor handles Syncthing-to-NAS sync. Key file stored locally, not synced. Windows Credential Manager remains the runtime cache for automation scripts; the vault is the archive.
Syncthing runs as a Docker container (port-remapped to 22001 due to a UGOS port conflict with the platform's built-in sync service). The NAS is the hub that keeps desktop and phone in sync even when neither endpoint is online simultaneously. No vault client runs on the NAS; it is a sync node only.
Syncthing-Fork + KeePassDX. The phone holds a local cached copy of the vault and syncs via Tailscale when it reaches the NAS. KeePassDX opens the local copy, so it works offline. Key file distributed out-of-band via USB during initial setup.
vault_audit.py scans all project repos for secret-shaped values in .env files and flags any that lack a corresponding vault entry. The script runs at session close as part of a standard ceremony. Findings are forwarded to Discord. Silent when clean.
The vault file lives on three devices and a NAS. The security perimeter is the combination of the passphrase and the key file. The key file is not on the NAS, which means a compromise of the NAS Docker volume yields an encrypted file that can't be opened without a separate endpoint. This is a standard KeePassXC defense-in-depth feature, applied deliberately.
Tailscale is the network trust boundary. All Syncthing traffic travels over the Tailscale mesh; Syncthing itself is not exposed to the public internet. The NAS Tailscale node is registered as an operator so tailnet commands don't require sudo.
Backup redundancy: five paths survive a single-point failure. Desktop image backup to a separate Synology NAS covers the desktop copy. The UGREEN Docker volume backup to the Synology NAS covers the hub copy. The phone holds a third live copy. None of this required new backup infrastructure; it was derived from what already existed.
Recovery: a printed recovery page in a physical location covers the "every digital device is gone simultaneously" scenario. It holds the passphrase and instructions. This is the cold-storage scenario the vault was designed for.
The vault is only useful if the credential surface that feeds it is visible and audited. That's what the automation layer does.
Scans all project repos for .env files and flags secret-shaped values (API keys, tokens, passwords, webhook URLs) that don't have a corresponding vault entry. Runs at session close; posts findings to Discord with a truncated value preview. Exit 0 when clean; exit 1 with findings when violations exist.
Slash command that walks through the full vault entry creation ceremony: prompts for all required metadata fields (Used-By, Keyring-Key, GH-Actions, Rotation-Notes), validates nothing is missing, then creates the entry. Enforces the convention that entry metadata tells you everywhere the secret lives.
Guided credential rotation: opens the existing vault entry, steps through updating each consumer listed in Used-By, and confirms the vault entry is updated with a rotation date and notes. Keeps the vault current when credentials change.
Automation scripts never load secrets from .env files. All runtime credentials are fetched from the OS keyring via secret_store.py. .env files hold non-secret config only (hostnames, ports, feature flags). The vault auditor enforces this at session close.
The same threat modeling I apply professionally (trust boundaries, defense-in-depth, least-privilege sync topology, recovery planning) applied to a personal home lab. The key-file-not-on-NAS decision is a small thing, but it's deliberate and documented.
Five commercial alternatives were evaluated and rejected. The design document records the reasoning for each rejection. "We looked at X, Y, and Z and chose this" is the right level of rigor for an infrastructure decision; "I just used what I knew" is not.
The vault is only as useful as the discipline around feeding it. Rather than relying on habits, the audit script enforces the policy mechanically at every session close. Violations surface immediately; clean passes are silent.
The vault doesn't live in a silo. It plugs into the existing Syncthing setup on the NAS, uses the existing Tailscale mesh, and integrates with the existing automation pipeline. Adding infrastructure that works with what's already there is harder than starting from scratch; it's also more realistic.
The vault file itself is not published anywhere; that would defeat the purpose. The automation scripts (vault_audit.py, secret_store.py) live in the AI Assistant repo as part of a larger personal automation ecosystem; they aren't extracted as a standalone library. Vault population (entering the ~155 audited candidates) is still in progress as a Phase 3 background task run at whatever pace is useful. The infrastructure is live; the data migration is ongoing.