Project

Credential Vault

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.

Personal project · Live since April 2026 · 3-device sync cluster · Automated audit surface

What it is

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.

Why this exists

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.

Architecture

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.

Desktop PC (primary editor)

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.

UGREEN NAS (always-on hub)

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.

Android phone (read-mostly)

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.

Audit automation

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.

Trust model

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 automation substrate

The vault is only useful if the credential surface that feeds it is visible and audited. That's what the automation layer does.

vault_audit.py

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.

/vault-new-secret skill

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.

/vault-rotate skill

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.

Windows Credential Manager integration

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.

What this demonstrates

Security architecture applied to personal infrastructure

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.

Tool selection with reasoning

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.

Enforced policy via automation

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.

Home lab integration

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.

Stack & scale

Stack

KeePassXC KeePassDX Syncthing (Docker) Tailscale Python Windows Credential Manager UGREEN NAS Discord webhooks

Scale

3-device sync cluster ~155 vault candidates audited All-Tailscale sync path No third-party cloud Session-close enforcement Live since April 2026

What's not here

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.