6.8 KiB
azos/nextcloud
Backlink: azos
Architecture
features/nextcloud/default.nix (home-manager module, azos.nextcloud-client /
azos.nextcloud-mount) mounts a Nextcloud webdav folder at ~/synced-docs via
rclone mount as a systemd user service (nextcloud-mount.service,
WantedBy = default.target, not graphical-session.target — the latter never
activates under EXWM since nothing explicitly starts it).
Credentials come from pass at service start (not baked into the Nix store):
password from pass zakobar.com, Nextcloud user ID (distinct from login name
on this server) from pass nextcloud-username/aner. See azos/pass.
Gated on YubiKey smartcard presence via the shared azos-card-present overlay
(overlays/default.nix, wraps gpg --card-status) — same silent presence
check used by the mbsync mail-sync timer (features/mail/default.nix). No card
-> loop quietly until plugged in, no pinentry prompt either way.
Restart = always means unplugging/replugging the key remounts naturally.
Added in commit 04427dd "Nextcloud sync, also zapzap" (2026-10-11), same commit
that bumped azos-core to add the zapzap WhatsApp client
(azos-core/features/station/whatsapp.nix) — unrelated features landed together.
Gotchas
PATH-override crash loop (fixed 2026-10-11)
nextcloud-mount.service crash-looped (exit code 127, every 10s) because
Service.Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin"; in
features/nextcloud/default.nix replaces the service's PATH rather than
appending. Every command in the mount script used absolute ${pkgs.X}/bin/...
paths except the YubiKey-poll loop's bare sleep 10 — which couldn't be found
under the restricted PATH, so bash exited 127. Ran fine manually (inherits full
interactive PATH), which is why it looked like it should work.
Fix: ${pkgs.coreutils}/bin/sleep 10 instead of bare sleep 10. General
lesson logged on the azos root —
any systemd unit that restricts PATH this way needs every bare command in its
script converted to an absolute nix-store reference, not just the ones the
override was added for.
Same crash loop recurred with a different command (2026-10-12)
nextcloud-mount.service was 127-looping again, but this time the culprit was
head, not sleep: password=$(pass zakobar.com | head -1) and the matching
uid=$(pass nextcloud-username/aner | head -1) line both piped into bare
head -1, which isn't on the restricted PATH either. Under set -eu, the
pipeline's exit status is head's (127), which aborts the script. Fixed by
changing both to ${pkgs.coreutils}/bin/head -1.
Confirms the general lesson above needs to be applied exhaustively: fixing the
one bare command that happened to be caught isn't enough — every token in
the script that isn't an absolute /nix/store/... path is a latent 127.
Next time this crash-loops, visually scan the whole script rather than
assuming the previously-fixed command is the only one.
Still 127'd after deploying the head fix — turned out mkdir -p
"$mountpoint" (the very first line of the script, before the YubiKey loop)
was also bare and had been bare the whole time, just masked because it
never got reached/tested in isolation before. Fixed to
${pkgs.coreutils}/bin/mkdir -p. Verified by extracting the deployed
writeShellScript output, manually running it under the exact restricted
PATH=/<fuse paths> from systemctl --user show nextcloud-mount.service -p
Environment, with bash -x to see exactly which line 127'd — this is the
reliable way to debug this script going forward rather than guessing from
journalctl alone (which only reports the exit code, not which line).
A third, different bug after that: non-setuid fusermount3 (2026-10-12)
After the mkdir fix the script got all the way to rclone mount, which
then failed with "fusermount3: mount failed: Operation not permitted" (exit
1, not 127 — a new failure mode). Root cause: NixOS's real, usable
fusermount3 is the setuid-root one at /run/wrappers/bin/fusermount3
(installed via security.wrappers); the plain ${pkgs.fuse3}/bin/fusermount3
in the nix store has no setuid bit and can't perform the mount syscall
(lacks CAP_SYS_ADMIN). rclone mount looks up fusermount3 via PATH
internally rather than an absolute path, and the service's
Service.Environment = "PATH…";= override pointed only at the raw
pkgs.fuse3=/=pkgs.fuse bin dirs, never /run/wrappers/bin — so it always
resolved to the broken non-setuid binary. ExecStop had the identical bug
(hardcoded to the non-setuid ${pkgs.fuse3}/bin/fusermount3 -u ...).
Fix: prepend /run/wrappers/bin to the PATH override, and point
ExecStop at /run/wrappers/bin/fusermount3 directly. Verified by manually
exporting the rclone env vars and PATH and running rclone mount by hand —
failed identically without /run/wrappers/bin, succeeded (real files
visible) with it.
General lesson for any systemd unit that overrides PATH and touches
FUSE: /run/wrappers/bin must be included, not just the raw package's
/bin — several NixOS tools (fusermount, sudo, etc.) only work from their
setuid wrapper, which lives there, not in the nix store path itself. I
initially misdiagnosed this same "Operation not permitted" error as an
artifact of my own sandboxed test shell lacking mount capabilities — it
was not; always re-test with the real setuid wrapper on PATH before
concluding "not reproducible" for FUSE-related permission errors.
Notably, the symptom this time didn't look like a crash-looping systemd
service at first glance — it looked like the Nextcloud desktop GUI client
(the separate services.nextcloud-client, webflow login) showing only the
Activity/notifications view with no file browser. That GUI client authenticates
fine on its own (confirmed via ~/.config/Nextcloud/logs: AccountState
CredentialsNotReady -> Connected) — it's unrelated to file access. Actual
file access goes through the rclone webdav mount at ~/synced-docs, which
was empty because this crash loop kept it unmounted. When the GUI client
looks "connected but empty", check systemctl --user status
nextcloud-mount.service rather than assuming an auth/account problem.
Related: zapzap notification delivery (confirmed working, no bug)
Same session also investigated zapzap (WhatsApp client, added alongside this feature) not showing desktop notifications. Confirmed the whole pipeline is correctly wired — see azos/exwm for the EDNC/D-Bus notification architecture note. No bug found; likely explanation if it recurs is WhatsApp Web's standard behavior of suppressing notifications for a chat that's already focused/open in the window.