The idea was simple

All I wanted was for my T620 to mount the QNAP shares, retire the QNAP as the “brain,” and have the T620 do everything … Samba, SFTP, the lot … while the QNAP just sat there as dumb storage in the background. Simple networking. I have never been more wrong about something taking one evening.

Boot failure #1: the NFS ordering cycle

First attempt, I set up NFS mounts from the QNAP onto the T620, then re-exported those same paths back out via NFS so other machines could reach them through the T620. Seemed logical … one machine, one point of access.

Rebooted. Box wouldn’t come up. Emergency mode, root mounted read-only, the whole nightmare. Turns out re-exporting an NFS-mounted path via NFS again is a genuine, documented bug in nfs-utils/systemd … not a Fedora thing, not something I did wrong, just a real contradiction in how nfs-server-generator builds its ordering dependencies. The mount wants to start before the server, the server wants to start before the mount, and systemd just gives up.

Fixed it by SSHing in, remounting root read-write, ripping out the bad config. Rebooted again. Same thing happened again a day later because the underlying fstab options (x-systemd.automount mixed with re-export) were still fighting each other.

The actual fix: stop trying to be clever. QNAP mounts get pulled onto the T620 via plain NFS with soft,timeo=30,retrans=3 … no automount, no hard, nothing fancy. Anything that needs to go back out to other machines goes out via Samba, never NFS-over-NFS again. Sounds obvious now. Took two boot failures to get there.

sudo tee -a /etc/fstab << 'EOF'
192.168.0.xxx:/Public   /mnt/data/Public   nfs   _netdev,nfsvers=3,soft,timeo=30,retrans=3   0 0
EOF
sudo systemctl daemon-reload
sudo mount -a

The permissions rabbit hole

Once the mounts were stable, I hit an access problem that made no sense on paper. Some files in one of my shares just would not open … ACL said I had full access, getfacl showed rwx right there in black and white, and Samba still handed back access denied.

Turns out the QNAP’s NFSv3 server was enforcing raw UID matching underneath the ACL, and completely ignoring what the ACL said for any UID that wasn’t the file’s original owner. My Linux UID (1000) didn’t match the QNAP-side UID (502) that actually owned those files, and no amount of setfacl fixed it because NFSv3 doesn’t do username mapping … just raw numbers, and QTS wasn’t respecting the ACL grant for a non-owning UID.

The fix: create a dummy local account with that exact UID (502), and tell Samba to force user = qnap-relationships on that specific share. Now Samba always accesses those files as the UID the QNAP actually trusts, no matter who’s really connected. Not elegant, but it works, and now I know to check ls -la for a raw number instead of a name … that’s the tell that a UID isn’t mapping.

sudo useradd -u 502 -M -s /sbin/nologin -g users qnap-relationships

In smb.conf, under the affected share:

[RELATIONSHIPS]
    force user = qnap-relationships
sudo systemctl reload smb

The HDD that ate my shares

Added a second local drive for shared storage. Mounted it straight onto /mnt/data … the same parent directory the QNAP shares were nested under. Instant chaos. Every share came back blank, Samba started throwing canonicalize_connect_path failed, and for about twenty horrible minutes I thought I’d lost data.

Nothing was lost. The new disk was just shadowing the QNAP submounts … mount something on top of a directory, and whatever was already mounted underneath temporarily disappears from view. Classic Linux mount behaviour, obvious in hindsight, invisible in the moment when you’re staring at empty folders at 11pm.

The fix: local disks get their own completely separate mountpoint. Always. /mnt/data2, not anywhere near /mnt/data. Never again.

sudo mkdir -p /mnt/data2
sudo tee -a /etc/fstab << 'EOF'
UUID=xxxxxxxxxxxxxxxxxxxxxxx   /mnt/data2   xfs   defaults,nouuid   0 0
EOF
sudo mount -a

(nouuid also fixes a related “duplicate UUID” mount refusal if the same disk ever gets reconnected via a different cable or port.)

SELinux, twice

Got bit by SELinux context mismatches more than once. First time, a directory needed to serve both Samba and SFTP, and the default samba_share_t context only satisfied one of those … SFTP kept getting silently denied until I relabeled it to public_content_rw_t, which happily serves both. Second time, same story with the Hugo web root for this very blog … nginx got a flat 403 until I ran the same relabel pattern.

Now it’s muscle memory: anything new that needs to be readable across multiple services gets checked with ls -Z before I even bother debugging further.

sudo semanage fcontext -a -t public_content_rw_t "/mnt/data2/Public(/.*)?"
sudo restorecon -Rv /mnt/data2/Public

SFTP jails and the “forgot to persist it” trap

Built proper SFTP chroot jails so each user gets dropped into just their own home folder plus a shared Public folder, nothing else visible. Worked great … until a reboot, at which point every user’s SFTP access to Public just vanished with no error, nothing in the logs pointing at why.

Turned out the bind mounts that made the jails work were only ever done live with mount --bind, never actually written anywhere permanent. A clean reboot means a clean slate, and nothing was there to redo the binding.

The fix, eventually: stopped hand-writing fstab lines per user entirely … at four users it was already unmanageable, and I was thinking about a hundred. Wrote one small systemd service that scans /srv/sftp/* at boot and binds whatever it finds, automatically. Add a new user, their jail just works next boot. Zero fstab edits, ever again.

sudo tee /usr/local/bin/sftp-bind-jails.sh << 'EOF'
#!/bin/bash
HOMES_ROOT="/mnt/data/homes"
PUBLIC_ROOT="/mnt/data2/Public"
SFTP_ROOT="/srv/sftp"
for jail in "$SFTP_ROOT"/*/; do
    user=$(basename "$jail")
    mkdir -p "${jail}home" "${jail}public"
    mountpoint -q "${jail}home"   || mount --bind "${HOMES_ROOT}/${user}" "${jail}home"
    mountpoint -q "${jail}public" || mount --bind "$PUBLIC_ROOT" "${jail}public"
done
EOF
sudo chmod +x /usr/local/bin/sftp-bind-jails.sh
sudo tee /etc/systemd/system/sftp-bind-jails.service << 'EOF'
[Unit]
Description=Bind-mount all my SFTP chroot jails
After=mnt-data-homes.mount mnt-data2.mount

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/sftp-bind-jails.sh

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now sftp-bind-jails.service

What I’d tell past-me

  • If you’re mounting something via NFS, check whether you’re about to re-export it. If yes, stop, use Samba instead.
  • A local disk needs its own mountpoint. Not near anything else.
  • getfacl showing the right answer doesn’t mean the underlying protocol is going to respect it. Check the raw UID.
  • Anything serving through more than one protocol needs its SELinux context checked before you assume it’s a permissions bug elsewhere.
  • Never trust a live mount --bind to survive a reboot. If it matters, it goes in a config file or a service, not just a command you ran once.

None of this was smart engineering. It was mostly stubbornness and a lot of journalctl at hours I’m not proud of. But the box works now, and I understand every single piece of why, which is worth more than if it had just worked first try.