From 00e095a63a2b9d92f0e641297138175258899f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 11:18:39 -0700 Subject: [PATCH 01/10] feat(glyph): add Documents Samba share for partner backups Adds a `partner` system user, a ZFS filesystem at zdata/documents, and a new Documents SMB share owned by that user. The hourly permissions service is updated to keep the mount correctly owned. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: cfc8816bb650 --- hosts/glyph/default.nix | 10 ++++++++++ hosts/glyph/services/samba.nix | 3 +++ 2 files changed, 13 insertions(+) diff --git a/hosts/glyph/default.nix b/hosts/glyph/default.nix index b4091541..f1a3a6ee 100644 --- a/hosts/glyph/default.nix +++ b/hosts/glyph/default.nix @@ -29,6 +29,16 @@ device = "zdata/backup"; fsType = "zfs"; }; + fileSystems."/mnt/documents" = { + device = "zdata/documents"; + fsType = "zfs"; + }; + + users.users.partner = { + isSystemUser = true; + group = "users"; + description = "Partner's Samba user"; + }; networking.hostName = "glyph"; networking.hostId = "e7bdc076"; # Ensure correct ZFS pool imported diff --git a/hosts/glyph/services/samba.nix b/hosts/glyph/services/samba.nix index 7cd7d795..609612dd 100644 --- a/hosts/glyph/services/samba.nix +++ b/hosts/glyph/services/samba.nix @@ -21,6 +21,7 @@ shares = { Archive = mkShare "/mnt/archive" "mu" "users"; Backup = mkShare "/mnt/backup" "mu" "users" // {"fruit:time machine" = "yes";}; + Documents = mkShare "/mnt/documents" "partner" "users"; Media = mkShare "/mnt/media" config.services.jellyfin.user config.services.jellyfin.group; Notes = mkShare "/var/lib/basic-memory/basic-memory" "basic-memory" "basic-memory"; Torrents = mkShare "/mnt/torrents" config.services.transmission.user config.services.transmission.group; @@ -65,11 +66,13 @@ Type = "oneshot"; ExecStart = let defaultUsrGrp = "mu:users"; + partnerUsrGrp = "partner:users"; jellyfinUsrGrp = with config.services.jellyfin; "${user}:${group}"; transmissionUsrGrp = with config.services.transmission; "${user}:${group}"; in [ "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} archive" "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} backup" + "${pkgs.coreutils}/bin/chown -R ${partnerUsrGrp} documents" # N.B.: /mnt/media/Music is used by Roon, not Jellyfin "${pkgs.coreutils}/bin/chown -R ${jellyfinUsrGrp} media/Movies media/TV media/Unsorted media/Video" "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} media/Music" From 14da9e58f0ee0f6931a3c996d8da8e000de30826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 11:43:09 -0700 Subject: [PATCH 02/10] feat(glyph): add WebDAV share at docs.zx.dev for partner access Serves /mnt/documents over WebDAV via nginx on glyph (port 8185, Tailscale-only), proxied through spore as docs.zx.dev with HTTPS and basic auth. Both Windows and macOS can mount this natively without any client software. Note: hosts/spore/secrets/docs-htpasswd.age must be created with agenix before deploying spore. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 1935831ebf09 --- hosts/glyph/services/default.nix | 1 + hosts/glyph/services/webdav.nix | 31 ++++++++++++++++++++++++++++ hosts/spore/services/web/default.nix | 19 +++++++++++++++++ lib/secrets/spore.nix | 1 + 4 files changed, 52 insertions(+) create mode 100644 hosts/glyph/services/webdav.nix diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index 468d631a..db05bb11 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -20,6 +20,7 @@ ./open-webui.nix ./prometheus.nix ./samba.nix + ./webdav.nix ./torrents.nix ./windmill.nix ]; diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix new file mode 100644 index 00000000..9f3d1259 --- /dev/null +++ b/hosts/glyph/services/webdav.nix @@ -0,0 +1,31 @@ +{pkgs, ...}: { + services.nginx = { + enable = true; + additionalModules = [pkgs.nginxModules.dav]; + virtualHosts."docs.zx.dev" = { + listen = [ + { + addr = "0.0.0.0"; + port = 8185; + } + ]; + locations."/" = { + root = "/mnt/documents"; + extraConfig = '' + dav_methods PUT DELETE MKCOL COPY MOVE; + dav_ext_methods PROPFIND OPTIONS; + dav_access user:rw group:rw; + create_full_put_path on; + client_max_body_size 0; + autoindex on; + ''; + }; + }; + }; + + users.users.nginx.extraGroups = ["users"]; + + systemd.tmpfiles.rules = [ + "d /mnt/documents 2775 partner users -" + ]; +} diff --git a/hosts/spore/services/web/default.nix b/hosts/spore/services/web/default.nix index 30a3e777..163ba73b 100644 --- a/hosts/spore/services/web/default.nix +++ b/hosts/spore/services/web/default.nix @@ -12,6 +12,11 @@ ]; age.secrets.cloudflare-dns.file = ../../secrets/cloudflare-dns.age; + age.secrets.docs-htpasswd = { + file = ../../secrets/docs-htpasswd.age; + owner = "nginx"; + mode = "400"; + }; services.nginx = { enable = true; @@ -126,6 +131,20 @@ useACMEHost = "zx.dev"; locations."/".proxyPass = "http://glyph.note-iwato.ts.net:8096"; }; + "docs.zx.dev" = { + forceSSL = true; + useACMEHost = "zx.dev"; + locations."/" = { + proxyPass = "http://glyph.note-iwato.ts.net:8185"; + extraConfig = '' + auth_basic "Documents"; + auth_basic_user_file ${config.age.secrets.docs-htpasswd.path}; + proxy_set_header Authorization ""; + client_max_body_size 0; + proxy_request_buffering off; + ''; + }; + }; "music.zx.dev" = { forceSSL = true; useACMEHost = "zx.dev"; diff --git a/lib/secrets/spore.nix b/lib/secrets/spore.nix index 7c414e3b..1a434d9d 100644 --- a/lib/secrets/spore.nix +++ b/lib/secrets/spore.nix @@ -2,6 +2,7 @@ let keys = with (import ../keys.nix); [spore Rhizome]; in { "hosts/spore/secrets/cloudflare-dns.age".publicKeys = keys; + "hosts/spore/secrets/docs-htpasswd.age".publicKeys = keys; "hosts/spore/secrets/homepage-env.age".publicKeys = keys; "hosts/spore/secrets/grafana-client-secret.age".publicKeys = keys; "hosts/spore/secrets/grafana-secret-key.age".publicKeys = keys; From bc402429527570c8bd94dc4285ec95c14efbcab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 11:49:58 -0700 Subject: [PATCH 03/10] fix(glyph): rename partner user to colleen Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: fd38d04ac1a5 --- hosts/glyph/default.nix | 2 +- hosts/glyph/services/samba.nix | 6 +++--- hosts/glyph/services/webdav.nix | 2 +- hosts/spore/secrets/docs-htpasswd.age | Bin 0 -> 391 bytes 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 hosts/spore/secrets/docs-htpasswd.age diff --git a/hosts/glyph/default.nix b/hosts/glyph/default.nix index f1a3a6ee..de7358db 100644 --- a/hosts/glyph/default.nix +++ b/hosts/glyph/default.nix @@ -34,7 +34,7 @@ fsType = "zfs"; }; - users.users.partner = { + users.users.colleen = { isSystemUser = true; group = "users"; description = "Partner's Samba user"; diff --git a/hosts/glyph/services/samba.nix b/hosts/glyph/services/samba.nix index 609612dd..29d0e0d2 100644 --- a/hosts/glyph/services/samba.nix +++ b/hosts/glyph/services/samba.nix @@ -21,7 +21,7 @@ shares = { Archive = mkShare "/mnt/archive" "mu" "users"; Backup = mkShare "/mnt/backup" "mu" "users" // {"fruit:time machine" = "yes";}; - Documents = mkShare "/mnt/documents" "partner" "users"; + Documents = mkShare "/mnt/documents" "colleen" "users"; Media = mkShare "/mnt/media" config.services.jellyfin.user config.services.jellyfin.group; Notes = mkShare "/var/lib/basic-memory/basic-memory" "basic-memory" "basic-memory"; Torrents = mkShare "/mnt/torrents" config.services.transmission.user config.services.transmission.group; @@ -66,13 +66,13 @@ Type = "oneshot"; ExecStart = let defaultUsrGrp = "mu:users"; - partnerUsrGrp = "partner:users"; + colleenUsrGrp = "colleen:users"; jellyfinUsrGrp = with config.services.jellyfin; "${user}:${group}"; transmissionUsrGrp = with config.services.transmission; "${user}:${group}"; in [ "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} archive" "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} backup" - "${pkgs.coreutils}/bin/chown -R ${partnerUsrGrp} documents" + "${pkgs.coreutils}/bin/chown -R ${colleenUsrGrp} documents" # N.B.: /mnt/media/Music is used by Roon, not Jellyfin "${pkgs.coreutils}/bin/chown -R ${jellyfinUsrGrp} media/Movies media/TV media/Unsorted media/Video" "${pkgs.coreutils}/bin/chown -R ${defaultUsrGrp} media/Music" diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index 9f3d1259..c600bb56 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -26,6 +26,6 @@ users.users.nginx.extraGroups = ["users"]; systemd.tmpfiles.rules = [ - "d /mnt/documents 2775 partner users -" + "d /mnt/documents 2775 colleen users -" ]; } diff --git a/hosts/spore/secrets/docs-htpasswd.age b/hosts/spore/secrets/docs-htpasswd.age new file mode 100644 index 0000000000000000000000000000000000000000..495bdeb1f7cbac4ff86b9ac7398c192a1034e4c3 GIT binary patch literal 391 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCSPa;!*C3{*%j%L;Id zD9w)w%k)W%D$~z3ud*mD%<@htt_ZV?3=Z=*HFOHfaW?k~G2qH7b~AI2DD>9P4)!Z8 z@b$}1btx=Lv54{v_VIJ^H?2x?3J>rw^~flyDn_@>*fl&OFHoT@*d(mnuOzEDGsq>w zHOR{~GsLhcEhwzmJu{-%pxm^g#GoY2Gcq{Bpq$GmGQck@G{7RuxID<;!qPR@G$Yit zEH%9_C_q2Qz|%O;FDOI1Fx0=&q?}7vS63mZ+`zQhET}xlBH19hDAPU5$;>(2IN3Bf zIW#EKD>19EtkNgP+uy~lAe8G?vbtDLCc^^NtW{^+{YkON?psXwd;m`$j* Date: Tue, 25 Aug 2026 12:04:36 -0700 Subject: [PATCH 04/10] fix(glyph): correct smb.conf security parameter name securityType is a NixOS option, not a smb.conf key. The correct key inside settings.global is `security`. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 8077d9a5bd73 --- hosts/glyph/services/samba.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/samba.nix b/hosts/glyph/services/samba.nix index 29d0e0d2..08dd8f3e 100644 --- a/hosts/glyph/services/samba.nix +++ b/hosts/glyph/services/samba.nix @@ -34,7 +34,7 @@ { global = { "invalid users" = []; - securityType = "user"; + security = "user"; "vfs objects" = "catia fruit streams_xattr"; "fruit:metadata" = "stream"; "fruit:resource" = "file"; From 9e5bc79a6fce172c97025a5177e16d43ca18a226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:08:51 -0700 Subject: [PATCH 05/10] fix(glyph): allow nginx to write to /mnt/documents for WebDAV NixOS nginx service uses ProtectSystem=strict; /mnt/documents must be explicitly listed in ReadWritePaths for MKCOL/PUT to succeed. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 5559f87c0c42 --- hosts/glyph/services/webdav.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index c600bb56..2e733025 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -25,6 +25,8 @@ users.users.nginx.extraGroups = ["users"]; + systemd.services.nginx.serviceConfig.ReadWritePaths = ["/mnt/documents"]; + systemd.tmpfiles.rules = [ "d /mnt/documents 2775 colleen users -" ]; From 4460754dbd74e198fc28df718c3b4b5d4d68ed2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:14:48 -0700 Subject: [PATCH 06/10] fix(glyph): fake LOCK/UNLOCK responses for macOS Finder WebDAV Finder requires a successful LOCK before allowing write operations. nginx does not implement LOCK/UNLOCK; return minimal valid responses to unblock folder creation and file uploads from macOS clients. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 852033c3abec --- hosts/glyph/services/webdav.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index 2e733025..0662f111 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -18,6 +18,16 @@ create_full_put_path on; client_max_body_size 0; autoindex on; + + # macOS Finder requires LOCK/UNLOCK to enable write operations. + # nginx does not implement them; return minimal valid responses. + if ($request_method = LOCK) { + add_header Content-Type 'application/xml; charset=utf-8'; + return 200 'infinitySecond-604800urn:uuid:fe184f2e-6eec-41d0-c765-01adc56113bb'; + } + if ($request_method = UNLOCK) { + return 204; + } ''; }; }; From d9a1f624a5cafa8a72abf43ee6f123c072b06931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:27:44 -0700 Subject: [PATCH 07/10] fix(glyph): advertise DAV level 2 for macOS WebDAV write access macOS WebDAVFS checks for DAV: 1, 2 in OPTIONS responses before enabling write operations in Finder. dav_ext_methods only sets DAV: 1; add the level 2 advertisement to match our faked LOCK support. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 823a25ea90f9 --- hosts/glyph/services/webdav.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index 0662f111..9d5e50c2 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -19,6 +19,10 @@ client_max_body_size 0; autoindex on; + # Advertise DAV level 2 so macOS WebDAVFS treats the share as writable. + # dav_ext_methods sets DAV: 1; this appends level 2. + add_header DAV '1, 2' always; + # macOS Finder requires LOCK/UNLOCK to enable write operations. # nginx does not implement them; return minimal valid responses. if ($request_method = LOCK) { From 702c6bbf805d8b61b1af34b8e2c999738d0c56e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:40:46 -0700 Subject: [PATCH 08/10] fix(glyph): replace nginx with Apache httpd for WebDAV nginx's dav module rejects MKCOL requests that carry a body, which macOS WebDAVFS sends as extended MKCOL (RFC 5689). Apache mod_dav handles extended MKCOL correctly and has full macOS compatibility. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 659245062958 --- hosts/glyph/services/webdav.nix | 48 +++++++++++++-------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index 9d5e50c2..c24607a0 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -1,47 +1,35 @@ -{pkgs, ...}: { - services.nginx = { +{_}: { + services.httpd = { enable = true; - additionalModules = [pkgs.nginxModules.dav]; + adminAddr = "admin@localhost"; + extraModules = ["dav" "dav_fs" "dav_lock"]; virtualHosts."docs.zx.dev" = { listen = [ { - addr = "0.0.0.0"; + ip = "*"; port = 8185; } ]; - locations."/" = { - root = "/mnt/documents"; - extraConfig = '' - dav_methods PUT DELETE MKCOL COPY MOVE; - dav_ext_methods PROPFIND OPTIONS; - dav_access user:rw group:rw; - create_full_put_path on; - client_max_body_size 0; - autoindex on; + documentRoot = "/mnt/documents"; + extraConfig = '' + DavLockDB /var/lib/httpd/DavLockDB - # Advertise DAV level 2 so macOS WebDAVFS treats the share as writable. - # dav_ext_methods sets DAV: 1; this appends level 2. - add_header DAV '1, 2' always; - - # macOS Finder requires LOCK/UNLOCK to enable write operations. - # nginx does not implement them; return minimal valid responses. - if ($request_method = LOCK) { - add_header Content-Type 'application/xml; charset=utf-8'; - return 200 'infinitySecond-604800urn:uuid:fe184f2e-6eec-41d0-c765-01adc56113bb'; - } - if ($request_method = UNLOCK) { - return 204; - } - ''; - }; + + DAV On + Options Indexes + AllowOverride None + Require all granted + + ''; }; }; - users.users.nginx.extraGroups = ["users"]; + users.users.wwwrun.extraGroups = ["users"]; - systemd.services.nginx.serviceConfig.ReadWritePaths = ["/mnt/documents"]; + systemd.services.httpd.serviceConfig.ReadWritePaths = ["/mnt/documents"]; systemd.tmpfiles.rules = [ "d /mnt/documents 2775 colleen users -" + "d /var/lib/httpd 0755 wwwrun wwwrun -" ]; } From 7131875201c261f0f379254a27fb3465eb8b00b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:45:43 -0700 Subject: [PATCH 09/10] fix(glyph): fix webdav.nix module arg pattern {_} is a single named arg, not a wildcard; NixOS modules need {...}. Use {lib, ...} to satisfy statix's empty_pattern check. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: fb0c6aa19e44 --- hosts/glyph/services/webdav.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/webdav.nix b/hosts/glyph/services/webdav.nix index c24607a0..639f53cf 100644 --- a/hosts/glyph/services/webdav.nix +++ b/hosts/glyph/services/webdav.nix @@ -1,4 +1,4 @@ -{_}: { +{lib, ...}: { services.httpd = { enable = true; adminAddr = "admin@localhost"; From ff3c13ccea5e0e936659d2b4a460ccd1361d597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Tue, 25 Aug 2026 15:52:17 -0700 Subject: [PATCH 10/10] fix(spore): rewrite WebDAV Destination header for MOVE/COPY Finder sends Destination: https://docs.zx.dev/... but Apache on glyph sees itself as http://docs.zx.dev:8185, causing a 502 on MOVE. Rewrite the Destination header to the internal URL before proxying. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 31842058822b --- hosts/spore/services/web/default.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hosts/spore/services/web/default.nix b/hosts/spore/services/web/default.nix index 163ba73b..d40726a3 100644 --- a/hosts/spore/services/web/default.nix +++ b/hosts/spore/services/web/default.nix @@ -142,6 +142,14 @@ proxy_set_header Authorization ""; client_max_body_size 0; proxy_request_buffering off; + + # Rewrite WebDAV Destination header for MOVE/COPY: Finder sends + # https://docs.zx.dev/... but Apache sees itself as http://docs.zx.dev:8185. + set $dav_dest $http_destination; + if ($dav_dest ~ "^https://docs\.zx\.dev(/.*)$") { + set $dav_dest http://docs.zx.dev:8185$1; + } + proxy_set_header Destination $dav_dest; ''; }; };