3f3c631057
Update PKGBUILD version / update-pkgver (push) Successful in 3s
Aligns with XDG Base Directory spec: $XDG_RUNTIME_DIR is the defined
location for non-essential runtime files (ephemeral, user-owned,
session-scoped). sshfs mounts fit that definition exactly, and the
tmpfs backing means orphaned mountpoint dirs vanish on logout instead
of accumulating.
- verify_mount_dir reads $XDG_RUNTIME_DIR, falls back to
/run/user/<uid>/ via os.Getuid().
- Existing path-traversal guard and symlink rejection carry over
unchanged.
- Tests switched from t.Setenv("HOME") to t.Setenv("XDG_RUNTIME_DIR").
File-manager sidebar visibility is unaffected — gvfs surfaces FUSE
mounts via /proc/mounts regardless of mountpoint location.
9.2 KiB
9.2 KiB
Decisions
2026-04-28 – Move mount base from ~/Servers/ to $XDG_RUNTIME_DIR/sshfs/
- Who: Dom, ClaudeCode
- Why:
~/Servers/is non-standard. XDG Base Directory spec defines$XDG_RUNTIME_DIR(=/run/user/<uid>/) for "non-essential runtime files" — ephemeral, user-owned, session-bezogen. sshfs mounts fit that definition exactly. systemd-logind creates the directory at login with mode 0700, so permissions are correct by construction. - Tradeoffs:
- Filemanager sidebar visibility is unchanged — gvfs surfaces FUSE mounts
via
/proc/mountsregardless of mountpoint location. - Tab-completion in
~/no longer reaches mounts; users targeting the mount via shell need the explicit$XDG_RUNTIME_DIR/sshfs/<alias>path. Acceptable given Dom's primary access is the filemanager sidebar. - tmpfs-backed → mounts and their containing dir vanish on logout/reboot. No more orphaned empty dirs. Tradeoff: bookmarks pinned to the absolute path stay valid because UID is stable across sessions, but the directory is gone between logins until the next mount recreates it.
- Fallback to
/run/user/<uid>/when$XDG_RUNTIME_DIRis unset (e.g. cron, non-login shells without systemd-logind). Falls auf einem System ohne systemd/run/user/<uid>/nicht existiert, schlägtMkdirAllmit klarem Fehler fehl — kein Silent-Fallback auf~/Servers/.
- Filemanager sidebar visibility is unchanged — gvfs surfaces FUSE mounts
via
- How:
verify_mount_dirreads$XDG_RUNTIME_DIR, falls back to/run/user/<uid>/viaos.Getuid()+strconv.Itoa.- Base =
<runtime>/sshfs, created withMkdirAll(0700). - Existing path-traversal guard (
EvalSymlinks+HasPrefix) and symlink rejection (Lstat) carry over unchanged. - Tests switched from
t.Setenv("HOME", ...)tot.Setenv("XDG_RUNTIME_DIR", ...).
2026-04-28 – Use ssh_config alias as label for mountpoint and sshfs source
- Who: Dom, ClaudeCode
- Why: File managers showed the resolved
HostName(often a raw IP) for both the mount source (user@10.0.0.5:) and the mountpoint directory (~/Servers/10.0.0.5/). The ssh_config alias is the human-readable label; using it makes mounts identifiable in Nautilus/Dolphin/etc. - Tradeoffs:
- sshfs now resolves the alias internally via
~/.ssh/configinstead of receiving a pre-resolved IP. Our explicit-o IdentityFile=and-p portstill win as overrides — the resolved values were redundant but kept as a belt-and-suspenders sanity check (early failure if config is malformed). - The CLI argument flows into the sshfs argv, so it must be validated.
Added
validate_ssh_field("alias", args[0], rxHostUser)immediately after arg parsing to gate injection-shaped input. - Existing mounts under
~/Servers/<ip>/are now orphaned. User must manuallyfusermount -uandrmdirthe IP-named directories. Documented in the README pointer.
- sshfs now resolves the alias internally via
- How:
args[0]captured intoalias, validated viarxHostUser.verify_mount_dir(alias)instead of(hostname); param renamed tonamefor clarity since it no longer represents a resolved hostname.mount_sshfsfirst arg renamedhostname → alias; argv source string becomesuser+"@"+alias+":"+remoteDir.
2026-04-28 – Add -r / --remote-dir flag for custom remote path
- Who: Dom, ClaudeCode
- Why: Mounting only the remote home was too restrictive; sometimes a
specific subdirectory (e.g.
/var/www,~/projects) is the actual target. - Tradeoffs:
- Two flag aliases (
-rand--remote-dir) registered against the same target viaflag.StringVar. Doubles theflag.PrintDefaultsoutput but matches typical short/long convention. rxRemoteDirdeliberately stricter thanrxIdentityFile— no:,@,+,=,%since remote paths rarely need them and looser allowlists invite injection-shaped surprises in theuser@host:pathargv slot.- Local mount path stays
~/Servers/<host>regardless of remote dir — one host = one mountpoint, even if-rdiffers between invocations. Re-mount requiresfusermount -ufirst.
- Two flag aliases (
- How:
rDirpackage-levelstring, registered ininit()asrandremote-dir; validated againstrxRemoteDir = ^[A-Za-z0-9/~][A-Za-z0-9._/~-]*$.mount_sshfssignature gainsremoteDir string; appended to theuser@host:argv slot. Empty string preserves the previous home-dir default.-voutput includesRemote:line whenrDiris set.- Tests extended with eight
rxRemoteDirboundary cases.
- Who: Dom, ClaudeCode
- Why: Audit found
kernel_cachecauses stale reads on a network FS;StrictHostKeyCheckingwas implicit (depended on system default);ssh_config-sourced strings flowed into the sshfs argv without validation, allowing comma- or leading-dash injection if~/.ssh/configis attacker-influenced. - Tradeoffs:
auto_cacheinvalidates the page cache onopen(2)when mtime/size differ → marginal perf hit vs. correctness on a network FS.accept-newforStrictHostKeyCheckingpreserves first-connection UX (TOFU) but locks subsequent reconnects to the recorded key.- Allowlist regexes for HostName/User/IdentityFile are restrictive but match real-world SSH naming. Rejected inputs surface a clear error.
- How:
- Replaced
-o kernel_cachewith-o auto_cacheinmount_sshfs. - Added explicit
-o StrictHostKeyChecking=accept-new. - Added
lookup_ssh_field(usesGetStrict, surfaces parse errors) andvalidate_ssh_fieldwith per-field allowlists; Port parsed viastrconv.Atoi. - Added
main_test.gocoveringverify_mount_dirand the allowlist regexes. - Hardened
.gitea/workflows/update-pkgver.yamlwith an empty-PKGVER guard.
- Replaced
2026-04-26 – LOW-severity audit findings: cleanup pass + one Won't-Fix
- Who: Dom, ClaudeCode
- Why: Followup on the LOW-severity findings from the same audit.
- How:
- Q-L1: dead
if len(port) == 0 { port = "22" }block already removed by Q-M1 refactor (port now flows throughlookup_ssh_field→strconv.Atoi). - Q-L2:
run_editorreturnserrorandmainexits 7 on editor failure instead of swallowing it. - Q-L3: ABOUTME header added to
main.goper global CLAUDE.md convention. - P-L1:
golang.org/x/sysbumpedv0.1.0 → v0.43.0viago get … && go mod tidy.godirective moved from 1.23.4 to 1.25.0 (required by the new dep). - S-L2: added
-vflag; default output now prints only the mount path.-vrestores the previous full HostName/User/Port/IdentityFile/Mount block.
- Q-L1: dead
- Won't-Fix:
- S-L1 —
.gitea/workflows/update-pkgver.yamlclones overhttp://gitea:3000/.... This URL only resolves inside the Docker network between the Gitea-act-runner container and the Gitea container. External traffic still terminates TLS at the reverse proxy. An attacker on the internal Docker bridge has already compromised the host; TLS between containers does not help in that scenario. Documenting as accepted risk.
- S-L1 —
2026-04-26 – Second-round audit: defense-in-depth + UX
- Who: Dom, ClaudeCode
- Why: Re-audit (Quality + Performance + Security in parallel) surfaced five
remaining LOW findings after agent-output gegencheck. Two MEDIUM claims were
filtered out as false positives: missing
Portalready defaults to"22"viassh_config.Default()(verified in library source), andlookup_ssh_fielddoes not parse~/.ssh/configfour times —kevinburke/ssh_configcaches viasync.Once, so fourGetStrictcalls = one parse + three in-memory walks. An additional MEDIUM claim about a regex range bug (%-interpreted as range) was empirically refuted (only-matched, not the implied range), but a legitimate defense-in-depth gap remained: the regex still allowed leading-in IdentityFile values. - Tradeoffs:
run_editorswitched tocmd.Start()— drops editor exit-status propagation in exchange for non-blocking UX on Sublime cold-start. Errors from missing binary or fork failure are still returned.verify_mount_dirnowMkdirAlls the base separately beforeEvalSymlinks.EvalSymlinksrequires the path to exist; without the upfrontMkdirAll, first run on a fresh system would fail.rxIdentityFilefirst-character class restricted to[A-Za-z0-9/~]. Plain relative paths like.ssh/idare rejected — unusual but technically valid inssh_config. If a real config uses relative IdentityFile values, the user gets a clear error and can switch to absolute or~-prefixed.
- How:
- S-L1: anchored
rxIdentityFileto^[A-Za-z0-9/~][A-Za-z0-9._@/:+=~%-]*$. - S-L2:
verify_mount_dirresolves base viafilepath.EvalSymlinksand rejects existing mount paths that are symlinks (os.Lstat). - Q-L1:
flag.Usageoverride now prints the positional<Host>argument in the help output. - Q-L2: inline comment at the
run_editorcall site documents the deliberate fall-through on already-mounted hosts. - P-L2:
run_editorusescmd.Start()instead ofcmd.Run(). - Tests extended: rxIdentityFile boundary cases,
TestVerifyMountDirRejectsSymlink, existingTestVerifyMountDirpaths now compared againstEvalSymlinks(base).
- S-L1: anchored