How To

Set Up a TFTP Server on Rocky Linux 10 / AlmaLinux 9 / RHEL

Decide one thing before you touch the server: will clients only pull files from it, or will switches and routers also push files back? The packaged Rocky Linux TFTP server answers that for you, because its systemd unit starts in.tftpd read-only. Every upload fails with Error code 1: File not found until you add the -c flag and a directory the daemon can write to. That default suits a PXE server; for config backups from network gear, it’s the first thing you’ll change.

Original content from computingforgeeks.com - post 31049

Below you’ll set up TFTP with the shipped tftp.socket, firewalld and SELinux enforcing, then add uploads, PXE files and faster transfers. The whole guide was run in October 2026 on Rocky Linux 10.1 against an Ubuntu 26.04 client, and the install, firewall and upload steps were repeated on AlmaLinux 9 (a 9.4 image pulling current el9_8 packages). RHEL is binary-compatible with both, but we didn’t run it.

What you need

TFTP has no login and no encryption, so it belongs on a trusted LAN or a provisioning VLAN, never on a public interface. The setup itself is small:

  • A Rocky Linux 10 or AlmaLinux 9 server with a sudo user
  • firewalld running and SELinux in enforcing mode
  • A client on the same network with the tftp client or curl
  • UDP port 69 open from the client to the server

Check the first two before you start, because cloud images often skip them. The Rocky 10 GenericCloud and AlmaLinux 9 images we used both shipped without firewalld, and the AlmaLinux image booted with SELinux in permissive mode. A TFTP setup tested on a box like that passes every check and then times out on a real install. If yours is missing it, install and start firewalld. Expect it to take a couple of minutes, because on Rocky the transaction also pulled in nftables and kernel-modules-extra:

sudo dnf install -y firewalld
sudo systemctl enable --now firewalld

If getenforce prints Permissive, switch SELinux to enforcing now and make it stick across reboots:

sudo setenforce 1 && sudo sed -i "s/^SELINUX=.*/SELINUX=enforcing/" /etc/selinux/config && getenforce

The sed edit is what keeps enforcing mode after a reboot, since setenforce alone only lasts until the next boot. A standard install from the Rocky ISO already runs firewalld with SELinux enforcing, so on those machines you can go straight to the install.

Install tftp-server and the tftp client

Both packages come from the AppStream repository on both distributions, not BaseOS as the old CentOS 8 version of this guide said. tftp-server holds the daemon and its systemd units. tftp is the client. Having it on the server lets you test locally before you involve the network:

sudo dnf install -y tftp-server tftp

The whole download is 73 KB, and the install took under two seconds on Rocky with metadata already cached. Both packages should show appstream as the source.

Installing:
 tftp                x86_64         5.2-50.el10         appstream          32 k
 tftp-server         x86_64         5.2-50.el10         appstream          40 k

Transaction Summary
================================================================================
Install  2 Packages

Total download size: 73 k
Installed size: 112 k
...
Complete!

The two distributions ship the same daemon with the same defaults. Only the package release and the firewalld major version differ:

ItemRocky Linux 10.1AlmaLinux 9 (9.4 image, el9_8 packages)
tftp-server package5.2-50.el105.2-40.el9_8.1
Repositoryappstreamappstream
in.tftpd -Vtftp-hpa 5.2, with remap, without tcpwrapperssame
Stock ExecStart/usr/sbin/in.tftpd -s /var/lib/tftpbootsame
/var/lib/tftpboot labeltftpdir_rw_ttftpdir_rw_t
firewalld2.4.31.3.4

Both still build the 5.2 line of tftp-hpa, although upstream has moved to the 7.x line on git.kernel.org. Nothing in this guide depends on the newer releases. Before you enable anything, look at what the package installed, because the unit files decide how you start the server.

systemctl cat tftp.service tftp.socket

The service has no WantedBy line, only Also=tftp.socket, and it reads its socket from standard input. Enabling it on its own would only enable the socket.

# /usr/lib/systemd/system/tftp.service
[Unit]
Description=Tftp Server
Requires=tftp.socket
Documentation=man:in.tftpd

[Service]
ExecStart=/usr/sbin/in.tftpd -s /var/lib/tftpboot
StandardInput=socket

[Install]
Also=tftp.socket

# /usr/lib/systemd/system/tftp.socket
[Unit]
Description=Tftp Server Activation Socket

[Socket]
ListenDatagram=69

[Install]
WantedBy=sockets.target

That design means systemd owns UDP port 69 and starts in.tftpd only when a request arrives. The socket is the unit you enable. Older CentOS guides copied these files to /etc/systemd/system/tftp-server.* and set the TFTP root to mode 777, and you need neither: a drop-in changes the flags later without forking the vendor units. The package also creates /var/lib/tftpboot as root:root 0755 with the SELinux type tftpdir_rw_t, which is the TFTP root for everything below.

Enable the tftp.socket listener

Right after install the socket is disabled and the service reports indirect, so nothing listens yet. Enable and start the socket:

sudo systemctl enable --now tftp.socket

systemd links it into sockets.target, which is what brings it back after a reboot. On AlmaLinux 9 the same message prints without the quotes around the paths.

Created symlink '/etc/systemd/system/sockets.target.wants/tftp.socket' → '/usr/lib/systemd/system/tftp.socket'.

Check the socket state and the port owner. You need sudo for ss -p here, because it only shows the owning process of a socket you are allowed to inspect.

systemctl status tftp.socket --no-pager
sudo ss -ulpn | grep :69

A healthy socket shows active (listening) on [::]:69 (Datagram), and ss shows systemd (pid 1) holding the port, not in.tftpd.

● tftp.socket - Tftp Server Activation Socket
     Loaded: loaded (/usr/lib/systemd/system/tftp.socket; enabled; preset: disabled)
     Active: active (listening) since Thu 2026-10-15 06:46:11 UTC; 195ms ago
 Invocation: c57712de28fd4ae4bfde0d23ccb37f1b
   Triggers: ● tftp.service
     Listen: [::]:69 (Datagram)
UNCONN 0      0                  *:69              *:*    users:(("systemd",pid=1,fd=57))

Don’t worry that systemctl is-active tftp.service says inactive at this point. The service starts on the first request and then stays up, because in.tftpd waits 900 seconds for another request before it exits (the -t default in its man page). We rebooted the Rocky box to confirm the behaviour survives. The socket came back active, the service stayed inactive until the client asked for a file, and that first download worked.

Open the TFTP service in firewalld

With firewalld running and the default public zone, a client request never reaches the socket. Your tftp-hpa client gives up after 25 seconds with Transfer timed out., and curl waits a full 5 minutes. firewalld ships a predefined tftp service, so add it permanently and reload:

sudo firewall-cmd --add-service=tftp --permanent
sudo firewall-cmd --reload

Each command prints success. Confirm the zone now lists the service, and look at what that service actually opens:

sudo firewall-cmd --list-services
cat /usr/lib/firewalld/services/tftp.xml

The service opens UDP 69 and attaches the tftp conntrack helper.

cockpit dhcpv6-client ssh tftp
<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>TFTP</short>
  <description>The Trivial File Transfer Protocol (TFTP) is a protocol used to transfer files to and from a remote machine in a simple way. It is normally used only for booting diskless workstations and also to transfer data in the Preboot eXecution Environment (PXE).</description>
  <port protocol="udp" port="69"/>
  <helper name="tftp"/>
</service>

We prefer the service to a bare --add-port=69/udp because it brings that helper along. TFTP answers each request from a new port (the man page calls it the Transaction ID), and the helper tracks that reply traffic as part of the original request. After the first transfer you will see nf_conntrack_tftp and nf_nat_tftp loaded in lsmod. If you want to limit TFTP to one subnet or move it to a dedicated zone, the firewalld guide for Rocky and AlmaLinux covers zones and rich rules. One small trap: firewall-cmd --version without sudo printed Authorization failed. on AlmaLinux 9, so run every firewall-cmd with sudo.

Test the Rocky Linux TFTP server from a client

Put a small file in the TFTP root so you have something to fetch. Writing it with tee inside the directory gives it the right SELinux label automatically, which matters more than it looks. The troubleshooting section shows what happens when you mv a file in instead.

echo "Hello from tftp01" | sudo tee /var/lib/tftpboot/hello.txt
sha256sum /var/lib/tftpboot/hello.txt

On an Ubuntu or Debian client, the client package is tftp-hpa. On another Rocky or AlmaLinux machine it’s tftp, which you installed on the server already. curl on both families speaks TFTP as well, so you can skip the client package if curl is all you have.

sudo apt-get install -y tftp-hpa

Set the server address once on the client so the rest of the commands paste as-is. Use your own server IP here:

export HOST_IP="10.0.1.254"

Fetch the file with the tftp client, then again with curl. Compare both checksums with the one you printed on the server, since a hash match is the only proof that the bytes arrived intact.

tftp "${HOST_IP}" -c get hello.txt && cat hello.txt && sha256sum hello.txt
curl -sS "tftp://${HOST_IP}/hello.txt" -o hello-curl.txt && sha256sum hello-curl.txt

Both transfers finished in under 20 ms on our LAN, and the hash matched the server’s copy.

Hello from tftp01
b67eca2b5d829b6de4922dac7ca903dcbd4e16999f7e6c55191d8f60dc82b9a0  hello.txt
b67eca2b5d829b6de4922dac7ca903dcbd4e16999f7e6c55191d8f60dc82b9a0  hello-curl.txt

Know two client habits before you script anything. The tftp-hpa client exits with status 0 even when the server answers with an error. A failed get therefore looks like success to your script, and it leaves a 0-byte file behind. curl returns a real error code (28, 68 or 71 in our tests), so use curl in scripts. The interactive client also starts in netascii mode, which rewrites line endings on the wire. Typing status at the tftp> prompt shows it:

tftp> Connected to 10.0.1.254.
Mode: netascii Verbose: off Tracing: off Literal: off
Rexmt-interval: 5 seconds, Max-timeout: 25 seconds

Our 18-byte hello.txt crossed the wire as 19 bytes in that mode. tftp-hpa converts it back on the receiving side. That is why even the 50 MB random file in the block-size section came through a default netascii get with a matching hash. Netascii still relies on both ends undoing the conversion, so use binary (octet) mode for boot files with -m binary, or type binary at the tftp> prompt.

Allow uploads (optional)

Skip this section if the server only hands out boot files. If devices need to push files to it, the stock unit refuses them. in.tftpd only lets clients overwrite files that already exist. A new upload fails with Error code 1: File not found from tftp-hpa and curl: (68) TFTP: File Not Found from curl. Fixing that takes a systemd drop-in with new flags and one directory owned by nobody, the user in.tftpd drops to after it starts.

Create the drop-in directory and write the override. The empty ExecStart= line clears the vendor command before the new one replaces it:

sudo mkdir -p /etc/systemd/system/tftp.service.d
printf "[Service]\nExecStart=\nExecStart=/usr/sbin/in.tftpd -c -p -U 022 -v -s /var/lib/tftpboot\n" | sudo tee /etc/systemd/system/tftp.service.d/override.conf

tee echoes the file it wrote, which is the complete /etc/systemd/system/tftp.service.d/override.conf. Nothing else goes in it, because every setting you leave out is inherited from the vendor unit.

[Service]
ExecStart=
ExecStart=/usr/sbin/in.tftpd -c -p -U 022 -v -s /var/lib/tftpboot

Each flag has a job. Only -c is required for uploads; the rest control the file mode and the logging:

  • -c lets clients create new files
  • -p skips the daemon’s own “global write” check
  • -U 022 pins the umask so new files land as 0644; without -p the default umask is zero and uploads come out world-writable 0666
  • -v logs every read (RRQ) and write (WRQ) request
  • -s /var/lib/tftpboot chroots into the TFTP root, as before

Our first upload run used only -c -v, and in.tftpd created the file as -rw-rw-rw-, which any local user can rewrite. Adding -U 022 fixed the mode, but with -U 022 and no -p, the server creates the file and then rejects it with Error code 2: File must have global write permissions, leaving a 0-byte file behind. Reload systemd and restart both units, because an in.tftpd left over from an earlier request keeps its old flags for up to 15 minutes:

sudo systemctl daemon-reload && sudo systemctl restart tftp.socket tftp.service && pgrep -a in.tftpd

The running process should carry the new flags. If pgrep still shows only -s /var/lib/tftpboot, the restart did not reach the old process.

27585 /usr/sbin/in.tftpd -c -p -U 022 -v -s /var/lib/tftpboot

Now give nobody one directory to write into. Keep the TFTP root itself owned by root, so a client can’t replace the boot files everyone else downloads:

sudo mkdir /var/lib/tftpboot/uploads && sudo chown nobody:nobody /var/lib/tftpboot/uploads && ls -ldZ /var/lib/tftpboot/uploads

The new directory inherits the tftpdir_rw_t label from its parent. That label is what lets SELinux allow the writes, so check it before you blame the daemon.

drwxr-xr-x. 2 nobody nobody unconfined_u:object_r:tftpdir_rw_t:s0 6 Oct 15 06:54 /var/lib/tftpboot/uploads

Push a file from the client into that directory. The echo prints the exit status, but tftp-hpa returns 0 either way, so the server is where you confirm it.

echo "switch01 config" > switch01.cfg
tftp "${HOST_IP}" -c put switch01.cfg uploads/switch01.cfg; echo "exit=$?"

On the server, list the directory with its labels and confirm the SELinux state. You want the file owned by nobody, not by your login.

ls -lZ /var/lib/tftpboot/uploads/; getenforce; getsebool tftp_anon_write

This output is from the AlmaLinux box, and Rocky gave the same result. The file belongs to nobody, it is 0644 because of -U 022, and it carries tftpdir_rw_t. SELinux is enforcing and the tftp_anon_write boolean is still off.

total 4
-rw-r--r--. 1 nobody nobody system_u:object_r:tftpdir_rw_t:s0 16 Oct 16 05:33 switch01.cfg
Enforcing
tftp_anon_write --> off

Plenty of guides tell you to turn on tftp_anon_write for uploads. You don’t need it under /var/lib/tftpboot, because the confined tftpd_t domain may already write to tftpdir_rw_t. The boolean matters only when the target directory is labelled public_content_rw_t, which you would do for a folder shared with an FTP, Samba or web server. We proved both sides: an upload into such a directory failed with an AVC denial until we ran sudo setsebool -P tftp_anon_write 1. Overwriting a root-owned file like hello.txt still fails with Error code 0: Permission denied, which is what you want. The client side of the download and upload looked like this:

TFTP client get and put against a Rocky Linux TFTP server from Ubuntu 26.04, with sha256 checks

The hello.txt checksum matches the server’s copy, so nothing was altered in transit. The last line is the 50 MB pull from the block-size section further down, which ran later in the same client session. The upload line shows exit=0, which only means something because you checked the file on the server.

Read the TFTP transfer log

The obvious command shows almost nothing. journalctl -u tftp.service prints only systemd’s start and stop lines, because the transfer messages come from short-lived in.tftpd children that the journal doesn’t attribute to the unit. Query them by syslog identifier instead:

sudo journalctl -t in.tftpd --since "-3min" --no-pager | tail -4

With -v in the drop-in, each transfer logs the request with the client address and the file, then a finished line when it completes. We captured this after the block-size runs further down, so the file in it is the 50 MB test file.

Oct 15 06:55:49 tftp01 in.tftpd[27664]: RRQ from ::ffff:10.0.1.230 filename test50M.bin
Oct 15 06:55:53 tftp01 in.tftpd[27664]: Client ::ffff:10.0.1.230 finished test50M.bin
Oct 15 06:55:53 tftp01 in.tftpd[27665]: RRQ from ::ffff:10.0.1.230 filename test50M.bin
Oct 15 06:55:54 tftp01 in.tftpd[27665]: Client ::ffff:10.0.1.230 finished test50M.bin

Uploads show up as WRQ from lines. Without -v you get only the finished lines, so a request that was refused leaves no trace at all, which makes the verbose flag worth keeping on a read-only server too. The ::ffff: prefix is just the IPv4 client address seen through the dual-stack socket. This capture, taken at the same point, shows the whole server side from the daemon version to the open firewall service and the log:

Rocky Linux TFTP server with tftp.socket listening on UDP 69, firewalld tftp service and in.tftpd journal

If a client reports success but nothing appears in this log, it is talking to a different server. Check the IP you exported on the client before you look at anything else.

Serve PXE boot files from the TFTP root

Network booting is the usual job for a TFTP server, and both boot paths have a trap. For UEFI clients, the signed shim and GRUB binaries come from the shim-x64 and grub2-efi-x64 packages, which our Rocky install already had. On a server installed in BIOS mode or from a minimal image, install them first (we didn’t need to run this line on our image):

sudo dnf install -y shim-x64 grub2-efi-x64

Copy them into a subdirectory and fix their mode in the same step, because cp keeps the 0700 mode they have on the EFI partition and nobody can’t read them (the SELinux label was already correct):

sudo mkdir -p /var/lib/tftpboot/uefi && sudo cp /boot/efi/EFI/rocky/shimx64.efi /boot/efi/EFI/rocky/grubx64.efi /var/lib/tftpboot/uefi/
sudo chmod 0644 /var/lib/tftpboot/uefi/*.efi && ls -l /var/lib/tftpboot/uefi/

The vendor directory name under /boot/efi/EFI/ follows the distribution. Expect almalinux on AlmaLinux and redhat on RHEL, but we ran this part on Rocky only, so list the directory first. Both files should now be world-readable.

total 4808
-rw-r--r--. 1 root root 3959112 Oct 15 06:56 grubx64.efi
-rw-r--r--. 1 root root  959224 Oct 15 06:56 shimx64.efi

BIOS clients need PXELINUX from syslinux-tftpboot, and that package installs its 64 files into /tftpboot with the tftpdir_t label, not into /var/lib/tftpboot where the server looks. Install it and copy the four files a basic menu needs:

sudo dnf install -y syslinux-tftpboot
sudo mkdir -p /var/lib/tftpboot/pxelinux.cfg && sudo cp /tftpboot/pxelinux.0 /tftpboot/ldlinux.c32 /tftpboot/menu.c32 /tftpboot/libutil.c32 /var/lib/tftpboot/

Fetch both UEFI loaders from the client in binary mode and compare them with the server’s checksums. Binary (octet) mode sends the bytes unchanged, with no conversion for the other end to undo.

tftp -m binary "${HOST_IP}" -c get uefi/shimx64.efi && tftp -m binary "${HOST_IP}" -c get uefi/grubx64.efi && sha256sum shimx64.efi grubx64.efi

Both hashes matched the copies in /var/lib/tftpboot/uefi. The shim took 0.199 s and the 4 MB GRUB binary 0.712 s, even with default 512-byte blocks.

b49c7d6911be1dc9ba05d9160d4f3ce8dc422cf454d11b6b0ea28ac2d81ffb30  shimx64.efi
bf9179f24177d4010f343944a97b1c6ec47816ef9c64a3cfe9d234afac89025f  grubx64.efi

The BIOS loader and its first module come down the same way. Both were copied from /tftpboot, so this also proves the copy kept a readable mode and label.

tftp -m binary "${HOST_IP}" -c get pxelinux.0 && tftp -m binary "${HOST_IP}" -c get ldlinux.c32 && ls -l pxelinux.0 ldlinux.c32

The sizes match the files in /tftpboot (42583 and 115644 bytes).

-rw-rw-r-- 1 josphat josphat 115644 Oct 15 06:57 ldlinux.c32
-rw-rw-r-- 1 josphat josphat  42583 Oct 15 06:57 pxelinux.0

This only makes the files available. A machine boots from them once DHCP points it at the server with next-server and a boot filename, which the ISC DHCP server guide sets up, and you will want a GRUB or PXELINUX menu plus an install tree. We didn’t boot a client in this lab. For a complete chain, the PXE and Kickstart walkthrough builds one end to end, and netboot.xyz with iPXE is the quicker route if you just want a menu of installers.

Speed up large transfers with a bigger block size

TFTP sends 512-byte blocks and waits for an acknowledgement after each one, so the default is slow for anything bigger than a boot loader. Clients can negotiate a larger blksize, and in.tftpd accepts up to 65464 bytes. The tftp-hpa client on Ubuntu has no option for it, so the timings below use curl’s --tftp-blksize.

Create a 50 MB random test file in the TFTP root first, and note its hash so you can check every download against it:

sudo dd if=/dev/urandom of=/var/lib/tftpboot/test50M.bin bs=1M count=50 status=none && sha256sum /var/lib/tftpboot/test50M.bin

Writing it in place with dd gives it the tftpdir_rw_t label, and this is the hash each download must match:

2b2b07f73439c2d777e1a4dfcf4819f5055d715da3961c81f1876b67d76ba4e1  /var/lib/tftpboot/test50M.bin

We pulled that file with each block size over a LAN with a 1450-byte MTU. The tftp-hpa row ran in the client’s default netascii mode:

Client and block sizeTime for 50 MB
tftp-hpa, 512, netascii (no option)10.236 s
curl default, 51210.515 s
curl --tftp-blksize 14224.190 s
curl --tftp-blksize 14684.224 s
curl --tftp-blksize 81921.002 s
curl --tftp-blksize 654640.304 s

The jump that matters is the first one: going from 512 to about 1.4 KB per block cut the time by more than half. Size that block with the man page’s rule, your smallest MTU minus 32 bytes (20 for IP, 8 for UDP and 4 for the TFTP header). That gives 1418 on our 1450-byte MTU and 1468 on a standard 1500-byte Ethernet. We timed 1422 before applying that rule. 1422 plus the 4-byte TFTP, 8-byte UDP and 20-byte IP headers is 1454 bytes, over the 1450 MTU, so those packets were fragmented and the transfer still worked.

The 8192 and 65464 runs fragmented every block and were faster still on our clean virtual LAN. The man page warns that some embedded clients request big blocks and then mishandle fragments, which is why it recommends capping the server with -B at that MTU-minus-32 value. We would use MTU minus 32 for firmware and switches, and allow larger blocks only for clients you have tested. Every hash matched. The 512-byte runs needed 102,400 blocks, past the 65,535 block-number limit, so block-number rollover worked with both clients.

This is the same pull sized by that rule for a 1450-byte MTU. We didn’t time 1418 itself; the table’s 1422 row is the closest measured run. Your curl must list tftp under Protocols in curl --version, as the Ubuntu build does.

curl -sS --tftp-blksize 1418 "tftp://${HOST_IP}/test50M.bin" -o test50M-curl1418.bin && sha256sum test50M-curl1418.bin

Change 1418 to your own MTU minus 32 (1468 on a 1500-byte MTU) if your network doesn’t use 1450. If the UDP side of this is new to you, our TCP vs UDP walkthrough shows why a protocol that acknowledges every block pays so heavily for small ones.

Fix TFTP timeouts and access violations

TFTP error messages are short and several causes share the same text, so match the exact message first and then the situation. These are the ones we triggered on purpose or by accident in the lab, with the fix that worked for each.

Transfer timed out. / curl: (28) TFTP response timeout

A timeout almost always means the request never reached the socket. In our lab it happened every time firewalld ran without the tftp service. tftp-hpa printed Transfer timed out. after 25.0 seconds and left a 0-byte local file, while curl took 5 minutes to print curl: (28) TFTP response timeout. Add the service as shown above, and check sudo firewall-cmd --list-services plus systemctl is-active tftp.socket on the server before you suspect anything else.

Error code 0: Permission denied on a file you moved in

A file created elsewhere and moved into the TFTP root keeps its old SELinux label. We built a file in a home directory and used mv, so it arrived as user_home_t, tftp-hpa reported Error code 0: Permission denied and curl reported curl: (71) TFTP: Illegal operation. The audit log names the label.

type=AVC msg=audit(1792047137.311:1035): avc:  denied  { read } for  pid=26210 comm="in.tftpd" name="moved.txt" dev="vda4" ino=117448584 scontext=system_u:system_r:tftpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

sealert suggested setsebool -P tftp_home_dir 1 with 89.3% confidence. That is the wrong fix. The file isn’t in a home directory any more, it only carries the label, and the boolean would let the daemon into home directories it never needs. Relabel the TFTP root instead:

sudo restorecon -Rv /var/lib/tftpboot

restorecon prints each file it changes, and the next download worked.

Relabeled /var/lib/tftpboot/moved.txt from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:tftpdir_rw_t:s0

Run sudo ausearch -m avc -ts recent at an interactive terminal. Run through a non-interactive ssh host 'command', it printed <no matches> for us even though the denial was in the log, because ausearch reads standard input when that isn’t a terminal. In scripts and ssh one-liners, sudo ausearch --input-logs -m avc -ts recent forces it to read the log files instead. The SELinux troubleshooting guide covers labels and booleans in more depth.

Error code 0: Permission denied with a correct label

When the label is right and there is no AVC, the denial is plain Unix permissions, because in.tftpd reads and writes as nobody. We hit four versions of it, and each one has a different fix:

  • Boot files copied from /boot/efi keep mode 0700, so chmod 0644 them
  • Uploads into a root-owned 0755 directory fail, so use the nobody-owned uploads/
  • Overwriting a root-owned file such as hello.txt fails by design
  • A public_content_rw_t target needs sudo setsebool -P tftp_anon_write 1 (this one logs an AVC)

To tell them apart quickly, run ls -lZ on the file and its directory and ask whether user nobody could do what the client asked. If the answer is yes and it still fails, go back to the AVC check above.

Error code 2: File must have global write permissions

This one appears when the daemon runs with -U 022 but without -p. in.tftpd creates the upload as 0644. Its own extra check then refuses to write a file that isn’t world-writable, and you’re left with a 0-byte file on the server. Add -p to the drop-in, delete the empty file, and restart both units.

Error code 1: File not found on upload

On a put, this means the daemon is running without -c, so it only accepts uploads that overwrite an existing file. curl shows it as curl: (68) TFTP: File Not Found. Add the drop-in from the upload section. If the error persists after daemon-reload, check pgrep -a in.tftpd, because an old process keeps serving with its old flags until you restart tftp.service or its 15-minute idle timer runs out.

With the socket enabled, the firewalld service added and SELinux left enforcing, that is the whole server. On either distribution we’d keep the stock read-only unit for PXE and add the upload drop-in only on a box whose job is collecting configs. If you’d rather serve install trees over NFS than HTTP once PXE works, the NFS server guide for Rocky and AlmaLinux picks up from here.

Keep reading

Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 AlmaLinux Patch PHP2Shell on RHEL, Rocky Linux and AlmaLinux 10 / 9 / 8 Customize KDE Plasma Desktop with Themes|Modules|Extensions AlmaLinux Customize KDE Plasma Desktop with Themes|Modules|Extensions Create and Configure Bridge Networking For KVM in Linux Virtualization Create and Configure Bridge Networking For KVM in Linux Best RHCSA Books for 2026: RHEL 10 (EX200) and RHCE Study Guides RHEl Best RHCSA Books for 2026: RHEL 10 (EX200) and RHCE Study Guides RHCSA Practice Questions: Hands-On EX200 Tasks with Solutions RHEl RHCSA Practice Questions: Hands-On EX200 Tasks with Solutions Configure mdadm Software RAID on Rocky Linux 10 / AlmaLinux 10 AlmaLinux Configure mdadm Software RAID on Rocky Linux 10 / AlmaLinux 10

Leave a Comment

Press ESC to close