Skip to content

darwin: auto-mount UNAS Pro 4 via nas.home.lan with a signed, attributed launchd agent - #32

Open
kriswill wants to merge 14 commits into
mainfrom
nas-mount
Open

darwin: auto-mount UNAS Pro 4 via nas.home.lan with a signed, attributed launchd agent#32
kriswill wants to merge 14 commits into
mainfrom
nas-mount

Conversation

@kriswill

@kriswill kriswill commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a services.nas-mount darwin module that auto-mounts the UNAS Pro 4's Personal-Drive SMB share at ~/nas via nas.home.lan at login, and takes the resulting launchd agent all the way to a clean System Settings > Login Items presentation: proper name (NasMount), custom icon, and real developer attribution (Kris Williams / Y6VCVC728W) instead of "Item from unidentified developer."

What's in here

  • modules/darwin/nas-mount.nix — host-selective module (enabled on k): launchd user agent (RunAtLoad + 5-min retry), deploys NasMount.app to ~/Applications with a stamp-guarded copy (a manual Developer ID signature survives routine nrs runs) + lsregister -f, and writes its plist via environment.userLaunchAgents because launchd.user.agents' closed serviceConfig submodule can't express AssociatedBundleIdentifiers.
  • pkgs/nas-mount/ — pure-std Rust mount helper (bare rustc -O, no Cargo) shipping both bin/nas-mount and an ad-hoc-signed NasMount.app (rcodesign in postFixup; an unsigned bundle is an invalid code object launchd rejects with EX_CONFIG). Icon rendered from an SF Symbol by the checked-in make-icon.swift.
  • scripts/sign-launchd-agents.ts (+ bin/ symlink, rcodesign in the dev shell) — fzf batch signer for ~/Library/LaunchAgents: shows per-agent signature status/authority/timestamp with a live codesign -dv preview, signs a whole selection off one user-supplied .p12 (exported single-identity via Keychain Access — security export can't filter and grabbed the wrong cert), and signs the enclosing .app when the executable lives in a bundle.
  • docs/unifi-dream-machine.md + knowledge bundle — the full investigation trail: keychain private keys are unreachable from root activation scripts (even via launchctl asuser), sops-committing the signing key rejected as disproportionate exposure, Developer ID signing a bare executable does nothing for Login Items (bundle association is the only path), BTM caches by plist path and only refreshes on remove+re-add, and assorted gotchas (lsof +D self-deadlocking SMB unmounts, sfltool dumpbtm auth-prompting per call).

Verification

  • Agent driven end-to-end: unmount → launchctl kickstart/bootstrap → keychain-auth mount (-N) → share browsable; exit 0.
  • Bundle codesign -v valid; Login Items verified live showing NasMount + icon + developer identity (BTM record: Developer Name: Kris Williams, Team Identifier: Y6VCVC728W).
  • nix flake check green; okf validate 168 files, 0 errors.

https://claude.ai/code/session_017uKm5pJE599j11mjfaG551

kriswill added 13 commits July 9, 2026 10:57
Add a launchd user agent (services.nas-mount, enabled on host k) that
mounts Personal-Drive at ~/nas over SMB using the nas.home.lan DNS
name set up earlier, with -N keychain-only auth and a StartInterval
retry since the NAS/network may not be up yet at login. Discovered
along the way that macOS refuses a second concurrent SMB mount of the
same share reached via a different hostname (Bonjour vs DNS) — it
dedupes by negotiated server identity, not the connect string.
writeShellScript put the script at the store path's top level, so its
hash-prefixed basename is what macOS shows in System Settings > Login
Items. writeShellScriptBin nests it under $out/bin/, so only the
directory carries the hash and the displayed name is plain
"nas-mount", matching how the other nix-built agents show up clean.
…manual path

Root-run activation scripts can't reach the login keychain's private
key (a real macOS security boundary, not a bug) — confirmed even with
launchctl asuser reattaching the session. Rejected the alternative of
sops-committing the exported Developer ID key for automated signing:
permanent exposure of a real signing identity for a cosmetic label
isn't worth it. nas-mount.nix now just copies the built script to a
stable path (cmp -s guarded so a manual signature survives routine
nrs runs); the actual signing is a manual, transient rcodesign + .p12
procedure the machine owner runs himself, documented in
docs/unifi-dream-machine.md and knowledge/decisions/nas-mount-codesigning.md.
sign-launchd-agents.ts scans ~/Library/LaunchAgents, shows each
agent's signature status (Developer ID / Apple-signed-no-team /
ad-hoc / unsigned) with an fzf multi-select and live codesign
preview, then signs a whole batch off one passphrase export instead
of repeating the manual procedure per agent. Same transient,
never-committed .p12 posture as the single-target script it
generalizes. Adds rcodesign to the dev shell.
Matches the existing bin/nix-pkgs-versions and bin/opencode pattern
so it runs as a bare command inside the dev shell.
cp preserves the nix store source's read-only mode, and chmod +x
only ever adds execute bits, so the copied file was permanently
r-xr-xr-x with no write bit -- rcodesign/codesign need to write here
to sign it. Fixed to chmod u+w,+x and moved outside the cmp -s
content-diff guard (permission bits don't affect a code signature)
so it self-heals every activation instead of only being set once.
Found by sign-launchd-agents correctly reporting the file read-only.
rcodesign errored "specified path is not of a recognized type" when
signing the shell-script version -- it only handles Mach-O/bundle/
DMG/pkg. Kept rcodesign over plain codesign (which would work fine
here) specifically because it never touches Keychain/session ACLs,
which is what will let this same signing approach run in GitHub CI
later with certs stored there securely (not implemented yet).

nas-mount is now pkgs/nas-mount/ -- pure-std Rust, no crates, built
via a bare rustc rather than rustPlatform.buildRustPackage's
Cargo.lock machinery -- registered as a real package (packages.nix +
overlays/nas-mount.nix) and referenced as pkgs.nas-mount instead of
writeShellScriptBin. mountPoint/share now pass as CLI args via
launchd's ProgramArguments. Bonus: rustc auto-ad-hoc-signs the
output at build time, upgrading Login Items from unsigned to ad-hoc
before any manual signing.
…tity

security export -t identities has no per-item filter -- it bundled
an unrelated "Apple Development" cert (Xcode) alongside "Developer
ID Application", and rcodesign silently signed nas-mount with the
wrong one instead of erroring on the ambiguity. Real (non-ad-hoc)
signature, but Login Items still showed unidentified developer
since Apple Development certs don't chain through the Developer ID
authority BTM checks for.

Both the script and the manual doc procedure now prompt for a .p12
path exported via Keychain Access.app (select exactly one identity)
instead of calling security export programmatically.
Found while end-to-end verifying nas-mount through its launchd agent:
lsof +D on an SMB mount hangs walking the tree and its own references
then hold the mount busy, so the unmount being debugged keeps failing
until the lsof processes are killed.
A correct Developer ID signature on the bare binary changed nothing --
BTM attributes launchd items only through an associated app bundle
(AssociatedBundleIdentifiers, per Apple DTS). pkgs/nas-mount now also
builds NasMount.app (bundle id net.kris.nas-mount, LSUIElement, icns
rendered from an SF Symbol by the checked-in make-icon.swift), ad-hoc
signed at build time via rcodesign in postFixup: an unsigned bundle is
an invalid code object launchd refuses with EX_CONFIG, and signing in
postInstall would be undone by fixupPhase's strip.

The module deploys the app to ~/Applications (stamp-guarded copy so a
manual Developer ID signature survives routine nrs runs) and runs
lsregister -f on it (cp-installed apps are invisible to LaunchServices,
which Login Items resolves the association through). The plist is
written via environment.userLaunchAgents because launchd.user.agents'
serviceConfig submodule is closed and lacks AssociatedBundleIdentifiers.
BTM caches by plist path and only re-reads on remove+re-add; the module
header documents the refresh dance. sign-launchd-agents now signs the
enclosing .app when a plist's executable lives inside one.

Verified live: Login Items shows NasMount with the custom icon and
Developer Name "Kris Williams" (Team Y6VCVC728W); agent exit 0, mount
intact. Notarization not required.
The codesigning knowledge from the nas-mount arc is general darwin/nix
material, not UDM material: identity/cert-type pitfalls, the
codesign-vs-rcodesign trust paths, the hard keychain boundaries, the
sign-launchd-agents tool, and the full Login Items attribution recipe
(.app bundle + AssociatedBundleIdentifiers + lsregister + BTM
cache-bust, notarization not required). unifi-dream-machine.md keeps a
pointer; knowledge-bundle references repointed.
Replace the manual sign-launchd-agents chore (for this one agent) with an
activation-time rcodesign step: sops-nix installs the dedicated signing PEM
at postActivation order 1500, the nas-mount block signs the ~/Applications
bundle at 1600 in the same switch — triggered by a fresh stamp-guarded copy
or a bundle whose codesign -dvv lacks Authority=Developer ID Application
(Authority, never TeamIdentifier). Failures warn without failing activation.

The identity is purpose-minted (openssl PKCS#8 key -> CSR -> Apple portal;
the personal Keychain identity is never exported) and sops-encrypted as
nas-signing-pem with owner k. Its declaration ships commented-out in the
host file until the key exists in secrets.yaml — sops-nix fails the build
otherwise (verified). Rotation = mint a new cert + replace the sops value.

Decision narrative: knowledge/decisions/nas-mount-codesigning.md
(2026-07-10 update); procedure: docs/darwin-codesigning.md.

Claude-Session: https://claude.ai/code/session_01Si4x5wbNVNuHgh4TyU66D8
…wn PR (#35)

The CI commits (ci.yml + update-flake-lock.yml + decision record) were
carried on this branch but belong in their own PR; they now live on
ci-builds (PR #35) with the FlakeHub cache integration on top. The
370592b commit had bundled this branch's codesigning log entry with the
CI log entry — restore the codesigning half here.

Transient: two dangling-link warnings (nas-mount-codesigning.md and
nas-mount.md -> ci-github-actions.md) until this branch merges main
after #35 lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant