Vaulthalla - Official Web Banner
All posts

The Final Cloud Comes to the Mac

Vaulthalla 1.13 runs natively on Apple Silicon: one brew install, a launchd service, bundled PostgreSQL and one-command HTTPS. How the port shipped in about a day, and hardened Linux on the way.

Cooper Larson6 min read

Vaulthalla runs on Apple Silicon now. Same daemon, same vault format, same permissions, same vh. One brew install and it's a system service on your Mac.

For most of Vaulthalla's life, "does it run on a Mac?" had a short answer: no. Vaulthalla leans on eventfd, SO_PEERCRED, umount2, O_TMPFILE, a TPM2, systemd, a Debian package and a FUSE driver that maps every Linux uid to a Vaulthalla identity. macOS has none of that.

A port felt like a quarter of work. It shipped in a little over a day: the macOS issue opened on October 12, and about twenty-five hours later v1.13.0 went out with an installer package, a Homebrew cask, launchd services, a bundled PostgreSQL, one-command HTTPS and a mount-free file CLI. Linux didn't lose a thing, and picked up a few fixes it didn't know it needed.


Install it first

1brew install --cask vaulthalla/tap/vaulthalla

Or grab vaulthalla-1.13.0-arm64.pkg from the release. Apple Silicon, macOS 14 or later. Then:

1sudo cat "/Library/Application Support/Vaulthalla/state/super_admin_initial_password"2open http://127.0.0.1:36968

Sign in as admin, create a vault, and you're running the same encrypted cloud as the Linux boxes. Details are in the macOS install guide.


Thirteen, not sixty

Before touching anything, the core went through a real Meson unity build on a Mac. 13 of roughly 60 compile units failed.

About fifteen Linux-only system calls, a few modern-toolchain gaps (Boost 1.89 dropped boost_system, libc++'s path::iterator isn't bidirectional), and one real libc++ hole: no std::atomic<std::shared_ptr>, which Vaulthalla's identity layer uses on its hottest path.

That turned "port it to macOS" from a vague dread into a backlog with names.

Seams, not #ifdefs

Every OS touchpoint moved behind a small interface in core/include/platform/: a waker instead of eventfd, peer credentials instead of SO_PEERCRED, accounts, child processes, mounts, and a sealer for secrets. Meson compiles platform/linux/ or platform/darwin/ and nothing else knows which one it got.

The Linux side of each seam is the old code, moved. The TPM2 sealing went across with git mv: same blobs, same behavior. The atomic shared_ptr wrapper is exactly std::atomic<std::shared_ptr> on libstdc++, enforced by a contract test, so the Linux hot path compiles to what it always did.

One code base, and as of this release it builds warning-free under -Werror with GCC and clang. CI now fails any build that prints anything but progress.


Linux got hardened by a Mac

A second compiler and a second standard library make a free audit, and it paid out:

  • A catch that could never run. libpqxx marks row[name] as [[gnu::pure]], so clang never executed the handler for a missing column. The code now checks first.
  • Undefined behavior in --help. Help rendering dereferenced an unset std::optional. It just happened to work on Linux.
  • Socket paths that silently truncated. Too-long CLI socket paths are now refused instead of cut short.

The best one came out of auditing the FUSE bridge for macFUSE: renameat2(RENAME_EXCHANGE) was being carried out as a plain rename. Ask the Linux mount to swap two files and it overwrote one with the other, tripped a unique constraint in the database, and left the file broken for later operations. The bridge now answers EINVAL for any rename flag it doesn't support.

That one was live on Linux in production, and a Mac port found it. apt upgrade picks up every fix; nothing else about your install changes.


Your files, without a mount

The bridge already builds against macFUSE, but Vaulthalla's mount checks every request against the caller's uid, and no macOS FUSE backend has proven it can say which local user is asking. No proof, no mount.

So 1.13 ships vh fs, on both platforms:

1vh fs ls  <vault> <path>2vh fs get <vault> <path> -o report.pdf3vh fs put <vault> ./notes.md /notes.md --overwrite

Bytes ride the CLI socket's existing frames, under the same permissions as the web console, with the same "not found vs. denied" answers so it can't be used to probe paths. Access is re-checked every 8 MiB of a download and right before an upload commits, so losing access mid-transfer stops the transfer. Downloads land atomically and never clobber your existing copy on failure; --overwrite refuses if the file changed since you were authorized to replace it.


A system service, not an app in your Dock

  • launchd, mirrored line by line from systemd. The daemons start at boot, before anyone logs in, and keep running when you log out.
  • A hidden _vaulthalla account with no shell, home or password runs every daemon. None runs as root, and its state directory is 0700 before a single secret is written.
  • Batteries included. PostgreSQL 16 built from sha256-checked upstream source, plus Node and Caddy. No Homebrew or Xcode tools needed.
  • You're the admin. The installing user is enrolled for vh and bound to admin automatically.
  • Boring upgrades. Reinstall over the top and every vault survives. uninstall.sh keeps your data unless you say --purge.

HTTPS that respects your trust store

1sudo vh setup caddy --domain vault.home.arpa --trust

For internal names, Vaulthalla mints its own certificate authority, name-constrained to your hostnames. Trust it on your phone and it can vouch for your vault and nothing else; in testing, a forged certificate for another domain signed by that CA was rejected. Public names get Let's Encrypt, through Cloudflare DNS if the Mac isn't reachable, or bring your own certificate.


Stories from the burner tests

Every macOS PR had to prove Linux untouched (105 of 105 FUSE integration tests, identical installed files, the full packaging contract suite), while the Mac side ran against throwaway PostgreSQL clusters, scratch install roots, stubbed system tools and clean, disposable runners. Some highlights:

  • The first real install broke because dscl prints dsAttrTypeNative: attributes the stubs didn't. The stubs now print the real thing.
  • An uninstall that pulled the floor out. The cask smoke test on a clean Mac found uninstall.sh printed its removal plan without applying it, then deleted the support directory under running daemons. Caught before any user ran it.
  • 94 of 94 acceptance checks passed, then the final teardown tripped over ${ROOT:?}. There's a test for that now.
  • 113 MB is a big package. The first upload to the package mirror got an HTTP 413 from the proxy. Not every finding is deep.

Every release now builds the package on Apple Silicon, installs it through the cask on a clean machine, checks the services, console and vh, then uninstalls, reinstalls and zaps before anything is published. None of it can block the Linux APT release.

And a tip of the cap to Claude Opus 5.5, my favorite junior-senior engineer, who carried a lot of this sprint and did it excellently.


What's not there yet

Know the limits before you move your vault to a Mac
  • No Finder mount yet. Use the web console, vh fs or the S3 gateway.
  • Keys aren't hardware-sealed on macOS. They live in 0600 files in a 0700 directory owned by the service account. Keep FileVault on; Secure Enclave sealing is planned.
  • The package isn't signed by Apple yet. Homebrew and sudo installer skip the prompt; from Finder it needs one Open Anyway.

Every platform difference is in the Platform Comparison. Everything else is the same Vaulthalla.


Where this goes

Until now, a Vaulthalla install meant one Linux box. As of today, it's one code base on whatever hardware you already own: the server in the closet, the Mac on the desk. When those machines start cooperating, a vault stops being a place and becomes something you keep in more than one. More on that soon.

Your hardware. Your keys. Your cloud. Now on your Mac too.