Archives, downloads and checksums

tar, compression, rsync and verifying files.

Beginner12 min · lesson 26 of 29

Sooner or later you need to move files: a copy of a configuration directory before you change it, a week of logs for a colleague, a release onto a server, a backup onto another machine. This lesson covers the tools for each part of that job: tar to bundle a directory into one file, gzip, xz and zstd to make it smaller, rsync to copy it to another machine over SSH, and curl with sha256sum and gpgv to download a file and prove it is the one its publisher released. The last part matters most for security: a download you have not verified is code you run on trust.

Bundling a directory with tar

tar packs many files and directories into one archive file, keeping their names, permissions, owners and modification times, and unpacks them again. It does not compress anything by itself; an option passes the archive through a compressor on the way. The practice directory for this lesson is a small web application, ~/cmd-archive/webapp, with a configuration file, a page and two logs. The commands below create it; the awk line writes 120,000 made-up access-log lines, and its details do not matter here:

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir -p ~/cmd-archive/webapp/config ~/cmd-archive/webapp/htdocs ~/cmd-archive/webapp/logs cd ~/cmd-archive/webapp printf 'listen = 127.0.0.1:8080\nworkers = 4\n' > config/webapp.conf printf '<h1>webapp</h1>\n' > htdocs/index.html printf '2026-09-27T09:14:03Z worker 3 restarted\n' > logs/error.log awk 'BEGIN { srand(42); for (i = 0; i < 120000; i++) printf "2026-09-27T%02d:%02d:%02dZ 198.51.100.%d GET /api/items/%d 200 %dms\n", int(i / 5000) % 24, int(i / 83) % 60, i % 60, int(rand() * 254) + 1, int(rand() * 5000), int(rand() * 900) + 3 }' > logs/access.log

The terminals in this lesson start each step with cd ~/cmd-archive, which is harmless to repeat in your own terminal.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive du -ah webapp
7.5M webapp/logs/access.log 4.0K webapp/logs/error.log 7.5M webapp/logs 4.0K webapp/config/webapp.conf 8.0K webapp/config 4.0K webapp/htdocs/index.html 8.0K webapp/htdocs 7.5M webapp

du -ah (disk usage, all files, human-readable sizes) shows that nearly all of it is access.log. tar's options are single letters: -c creates an archive, -t lists one, -x extracts one, -f NAME names the archive file, -v prints each file as it goes, and -z compresses with gzip. By convention a gzip-compressed tar archive ends in .tar.gz.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive tar -czf webapp.tar.gz webapp
$ cd ~/cmd-archive tar -tvf webapp.tar.gz
drwxrwxr-x deploy/deploy 0 2026-09-27 13:05 webapp/ drwxrwxr-x deploy/deploy 0 2026-09-27 13:05 webapp/logs/ -rw-rw-r-- deploy/deploy 7827758 2026-09-27 13:05 webapp/logs/access.log -rw-rw-r-- deploy/deploy 40 2026-09-27 13:05 webapp/logs/error.log drwxrwxr-x deploy/deploy 0 2026-09-27 13:05 webapp/config/ -rw-rw-r-- deploy/deploy 36 2026-09-27 13:05 webapp/config/webapp.conf drwxrwxr-x deploy/deploy 0 2026-09-27 13:05 webapp/htdocs/ -rw-rw-r-- deploy/deploy 16 2026-09-27 13:05 webapp/htdocs/index.html

Creating the archive prints nothing, which means it worked. The listing reads like ls -l: permissions, owner and group, size in bytes, modification time and name. Every name starts with webapp/ because that is how the directory was named on the command line, and each directory comes before the files inside it. List any archive you did not make before you extract it, so you know where its files will land.

-C DIR extracts into another directory instead of the current one. Extracting needs no -z: GNU tar recognises the compression format when it reads an archive.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive mkdir restore tar -xf webapp.tar.gz -C restore ls -l restore/webapp
total 12 drwxrwxr-x 2 deploy deploy 4096 Sep 27 13:05 config drwxrwxr-x 2 deploy deploy 4096 Sep 27 13:05 htdocs drwxrwxr-x 2 deploy deploy 4096 Sep 27 13:05 logs

When you archive files by their absolute path, tar removes the leading slash and says so:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive tar -czf ssh-config.tar.gz /etc/ssh/sshd_config /etc/ssh/sshd_config.d
tar: Removing leading `/' from member names tar: Removing leading `/' from hard link targets
$ cd ~/cmd-archive tar -tvf ssh-config.tar.gz
-rw-r--r-- root/root 4307 2026-08-31 18:36 etc/ssh/sshd_config drwxr-xr-x root/root 0 2026-09-27 10:24 etc/ssh/sshd_config.d/ -rw-r--r-- root/root 26 2026-09-18 16:38 etc/ssh/sshd_config.d/60-cloudimg-settings.conf -rw-r--r-- root/root 20 2026-09-26 19:49 etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf

The members are stored as etc/ssh/..., so extracting them can never overwrite the real /etc: they land under the directory you extract into, and you copy back what you need. The second message applies the same rule to hard links. (10-acceptenv-colorterm.conf is added by the lab's VM tool, not by Ubuntu.) The listing also shows that the archive recorded root/root as the owner. What happens to that depends on who extracts it:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive tar -xf ssh-config.tar.gz -C restore ls -l restore/etc/ssh/sshd_config
-rw-r--r-- 1 deploy deploy 4307 Aug 31 18:36 restore/etc/ssh/sshd_config

Extracted by deploy, the file belongs to deploy: an ordinary user always extracts files as themselves. Run as root (sudo tar -x), tar restores the owners and permissions recorded in the archive, which tar(1) lists as the default for the superuser. That is what you want when you restore your own backup of /etc, and dangerous with an archive from anyone else, because it can create root-owned or set-user-ID files wherever it is unpacked. List first, extract as yourself into an empty directory, and use sudo only for archives you made.

Compressing: gzip, xz and zstd

Compression finds repeated patterns and stores them once, and logs repeat themselves a lot. gzip FILE replaces the file with a compressed FILE.gz; -k keeps the original, and gunzip (or gzip -d) reverses it.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive gzip -k webapp/logs/access.log ls -l webapp/logs
total 8848 -rw-rw-r-- 1 deploy deploy 7827758 Sep 27 13:05 access.log -rw-rw-r-- 1 deploy deploy 1224543 Sep 27 13:05 access.log.gz -rw-rw-r-- 1 deploy deploy 40 Sep 27 13:05 error.log

The 7.8 MB log became 1.2 MB. Ubuntu Server 26.04 also installs xz and zstd (Zstandard), which tar uses with -J and --zstd. time -p in front of a command reports how long it took; real is the elapsed time in seconds.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive time -p tar -czf webapp.tar.gz webapp time -p tar -cJf webapp.tar.xz webapp time -p tar --zstd -cf webapp.tar.zst webapp
real 0.08 user 0.08 sys 0.01 real 2.92 user 2.74 sys 0.18 real 0.05 user 0.02 sys 0.04
$ cd ~/cmd-archive ls -l webapp.tar.*
-rw-rw-r-- 1 deploy deploy 1225195 Sep 27 13:05 webapp.tar.gz -rw-rw-r-- 1 deploy deploy 825860 Sep 27 13:05 webapp.tar.xz -rw-rw-r-- 1 deploy deploy 1283942 Sep 27 13:05 webapp.tar.zst

Even this small example shows the trade-off. xz made the smallest archive, a third smaller than gzip's, but took about 2.4 seconds against less than a tenth of a second. zstd at its default level was the fastest and within 5% of gzip's size. A reasonable rule: gzip when the archive has to open anywhere, zstd for large backups where time matters, and xz for files compressed once and downloaded many times, such as release archives. All three are lossless: the files come back byte for byte.

A file name's ending is only a convention. file reads the first bytes and names the real format, and tar needs no hint to read any of them:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive file webapp.tar.gz webapp.tar.xz webapp.tar.zst
webapp.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 7843840 webapp.tar.xz: XZ compressed data, checksum CRC64 webapp.tar.zst: Zstandard compressed data (v0.8+), Dictionary ID: None
$ cd ~/cmd-archive tar -tf webapp.tar.zst | head -n 3
webapp/ webapp/logs/ webapp/logs/access.log

Not every archiver is installed by default. On a fresh Ubuntu Server 26.04:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ command -v tar gzip xz zstd zip unzip
/usr/bin/tar /usr/bin/gzip /usr/bin/xz /usr/bin/zstd

command -v prints the path of each command it finds and nothing for the others, so zip and unzip are missing. You need them only to exchange files with people on Windows or macOS, where .zip opens with a double-click: sudo apt install zip unzip, then zip -r site.zip site. Do not rely on zip's password option (-e) for secrecy; zip(1) itself calls the standard zip encryption relatively weak, so encrypt with a proper tool such as gpg instead. On RHEL 10, tar, gzip and xz are present, but the Rocky Linux 10.2 cloud image used for this course's cross-checks has no zstd command until you install the zstd package.

Copying to another machine with rsync

rsync copies files and directories, locally or to another machine over SSH, and compares both sides first so that it sends only what changed. It has to be installed on both machines, and Ubuntu Server includes it. SSH handles the login and the encryption, so everything from "Connecting with SSH" applies: keys, the host key prompt on the first connection, and host aliases in ~/.ssh/config. The lab has only one machine, so the backup server is a second account on it, ca-backup, reached over SSH at 127.0.0.1. Create the account, a key without a passphrase for it (a backup job has nobody to type one), and install the public key in the account's authorized_keys with the right owner and modes (install -d creates a directory, install copies a file, both setting -m mode and -o/-g owner and group):

deploy@web01 · Ubuntu 26.04 LTS
$ sudo useradd -m -s /bin/bash ca-backup ssh-keygen -q -t ed25519 -N "" -C "deploy@web01 backups" -f ~/.ssh/backup01_ed25519 sudo install -d -m 700 -o ca-backup -g ca-backup /home/ca-backup/.ssh sudo install -m 600 -o ca-backup -g ca-backup ~/.ssh/backup01_ed25519.pub /home/ca-backup/.ssh/authorized_keys

Then add a host alias backup01 to ~/.ssh/config with an editor. The first connection asks about the host key, as in "Connecting with SSH"; for 127.0.0.1 it is this machine's own key, which ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub prints for comparison. (The lab recorded it beforehand, so the terminals below show no prompt.)

~/.ssh/config
Host backup01
HostName 127.0.0.1
User ca-backup
IdentityFile ~/.ssh/backup01_ed25519

On a real network, HostName would be the backup server's name or address. In the command, -a (archive) copies directories recursively and keeps permissions, modification times and symbolic links; owners are kept only when the receiving side runs as root. -v lists each file it sends, and backup01:webapp/ is a path relative to the remote account's home directory.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive rsync -av webapp/ backup01:webapp/
sending incremental file list created directory webapp ./ config/ config/webapp.conf htdocs/ htdocs/index.html logs/ logs/access.log logs/error.log sent 7,830,172 bytes received 144 bytes 5,220,210.67 bytes/sec total size is 7,827,850 speedup is 1.00

The first copy sends everything. Run it again, then once more after adding a line to the configuration file:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive rsync -av webapp/ backup01:webapp/
sending incremental file list sent 230 bytes received 15 bytes 490.00 bytes/sec total size is 7,827,850 speedup is 31,950.41
$ cd ~/cmd-archive echo "timeout = 30" >> webapp/config/webapp.conf rsync -av webapp/ backup01:webapp/
sending incremental file list config/webapp.conf sent 335 bytes received 44 bytes 758.00 bytes/sec total size is 7,827,863 speedup is 20,653.99

The second run sends nothing: rsync compares each file's size and modification time and skips the ones that match. The third sends only webapp.conf, and for a large file with a small change it would send only the changed parts. The speedup figure is the total size divided by the bytes actually sent and received. That is why rsync is the tool for repeated copies and deployments. A copy is not yet a backup, though: the next run faithfully copies an accidental deletion or a corrupted file over the good one, so keep older versions too (dated archives, or a backup tool), and back up a running database with its own dump or snapshot tool, not by copying its files while it writes them.

Two details cause most rsync accidents. A trailing slash on the source means "the contents of this directory"; without it, rsync copies the directory itself into the destination. -n (--dry-run) shows what would happen without changing anything:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive rsync -avn webapp backup01:webapp/
sending incremental file list webapp/ webapp/config/ webapp/config/webapp.conf webapp/htdocs/ webapp/htdocs/index.html webapp/logs/ webapp/logs/access.log webapp/logs/error.log sent 284 bytes received 44 bytes 656.00 bytes/sec total size is 7,827,863 speedup is 23,865.44 (DRY RUN)

Without the slash, the files would have landed in webapp/webapp/ on the backup. The second detail is --delete, which makes the destination an exact mirror by deleting files that no longer exist on the source. Aimed at the wrong directory, it deletes the wrong files, so run it with -n first:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive rm webapp/logs/error.log rsync -avn --delete webapp/ backup01:webapp/
sending incremental file list deleting logs/error.log logs/ sent 234 bytes received 43 bytes 184.67 bytes/sec total size is 7,827,823 speedup is 28,259.29 (DRY RUN)
$ ssh backup01 ls -l webapp/logs
total 7652 -rw-rw-r-- 1 ca-backup ca-backup 7827758 Sep 27 13:05 access.log -rw-rw-r-- 1 ca-backup ca-backup 40 Sep 27 13:05 error.log

The dry run announces deleting logs/error.log, and the backup still has the file because nothing was changed. For a single file, scp FILE backup01: also works. Since OpenSSH 9.0 it transfers over the SFTP protocol; -O selects the legacy SCP protocol for old servers that lack SFTP.

Downloading a file and proving it is genuine

curl downloads a URL. For files, combine -f (fail on an HTTP error instead of saving the server's error page under the file's name), -sS (no progress meter, but still print errors), -L (follow redirects) and -O (save under the name at the end of the URL). wget URL does much the same. Watch one trap between them: curl's -O takes no argument, while wget -O NAME saves under the name you give it.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive curl -fsSLO https://cloud-images.ubuntu.com/releases/resolute/release-20260918/SHA256SUM ls SHA256SUM
curl: (22) The requested URL returned error: 404 ls: cannot access 'SHA256SUM': No such file or directory

The number in parentheses, 22, is also curl's exit status, and ls shows that no file was saved. A script that checks the exit status of curl stops here instead of unpacking an HTML error page.

A checksum proves that a file is intact. SHA-256 turns any file into a 64-character fingerprint, and changing a single byte changes the fingerprint completely. Publishers list the fingerprints of their files in a checksum file. The example downloads a small file from the Ubuntu cloud image release this course's lab machines were built from, the image manifest (the list of packages in the image), along with the release's SHA256SUMS and its signature. The variable base only saves typing the long address three times.

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive base=https://cloud-images.ubuntu.com/releases/resolute/release-20260918 curl -fsSLO "$base/ubuntu-26.04-server-cloudimg-arm64.manifest" curl -fsSLO "$base/SHA256SUMS" curl -fsSLO "$base/SHA256SUMS.gpg" ls -l ubuntu-26.04-server-cloudimg-arm64.manifest SHA256SUMS*
-rw-rw-r-- 1 deploy deploy 8788 Sep 27 13:05 SHA256SUMS -rw-rw-r-- 1 deploy deploy 833 Sep 27 13:05 SHA256SUMS.gpg -rw-rw-r-- 1 deploy deploy 20277 Sep 27 13:05 ubuntu-26.04-server-cloudimg-arm64.manifest
$ cd ~/cmd-archive sha256sum ubuntu-26.04-server-cloudimg-arm64.manifest grep cloudimg-arm64.manifest SHA256SUMS
b06f1a7e0ca91f5c10409b71aa92b9c2841db2ad1dda60c96117953477ef499f ubuntu-26.04-server-cloudimg-arm64.manifest b06f1a7e0ca91f5c10409b71aa92b9c2841db2ad1dda60c96117953477ef499f *ubuntu-26.04-server-cloudimg-arm64.manifest
$ cd ~/cmd-archive sha256sum -c --ignore-missing SHA256SUMS
ubuntu-26.04-server-cloudimg-arm64.manifest: OK

The two fingerprints are identical; the * in the published list marks binary mode and changes nothing for the check. sha256sum -c does the comparison for every file named in SHA256SUMS, and --ignore-missing skips the many files you did not download. Change one line of the manifest and the check fails, with exit status 1:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive echo "extra-package 1.0" >> ubuntu-26.04-server-cloudimg-arm64.manifest sha256sum -c --ignore-missing SHA256SUMS
ubuntu-26.04-server-cloudimg-arm64.manifest: FAILED sha256sum: WARNING: 1 computed checksum did NOT match sha256sum: SHA256SUMS: no file was verified

A checksum only tells you that the file matches the list. Someone who controls the download server can replace the file and the list together. A signature closes that gap. Canonical signs SHA256SUMS with its cloud image key, and the public half of that key came with your server in /usr/share/keyrings, from the ubuntu-keyring package. gpgv checks a signature against keys you already have:

deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/cmd-archive gpgv --keyring /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg SHA256SUMS.gpg SHA256SUMS
gpgv: Signature made Sat Sep 19 00:44:15 2026 UTC gpgv: using RSA key D2EB44626FDDC30B513D5BB71A5D6C4C7DB87C81 gpgv: Good signature from "UEC Image Automatic Signing Key <cdimage@ubuntu.com>"
$ cd ~/cmd-archive sha256sum ubuntu-26.04-server-cloudimg-arm64.manifest > forged.sums cat SHA256SUMS >> forged.sums gpgv --keyring /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg SHA256SUMS.gpg forged.sums
gpgv: Signature made Sat Sep 19 00:44:15 2026 UTC gpgv: using RSA key D2EB44626FDDC30B513D5BB71A5D6C4C7DB87C81 gpgv: BAD signature from "UEC Image Automatic Signing Key <cdimage@ubuntu.com>"

Good signature means SHA256SUMS is exactly what the key's owner signed. The forged list, made by adding the changed manifest's own checksum, fails with BAD signature and exit status 1. So the order is: check the signature on the list, then check the file against the list. Package repositories apply the same chain to every package they serve, which is one reason to prefer them to loose downloads.

Before you run or unpack a download
1Download to a file
curl -fsSLO, never into a shell
2Check the signature
gpgv on the checksum list
3Check the checksum
sha256sum -c against the list
4Read it
less, if it is a script
5Run or unpack it
as yourself, in an empty directory
A checksum from the same server proves the file is intact, not who made it.
Never pipe a download into a shell
Install instructions such as curl -fsSL URL | sudo bash run whatever the server sends, as root, before anyone has read or verified it, and if the connection drops, bash runs whatever part arrived. Download the script to a file, verify it, read it, and only then run it. Better still, install from the vendor's signed package repository, as the next lesson shows.

Try this

Using the backup01 account from this lesson, make a checksummed backup and prove it survived the trip. Create webapp.tar.gz, write its checksum with sha256sum webapp.tar.gz > webapp.tar.gz.sha256, copy both with rsync -a webapp.tar.gz webapp.tar.gz.sha256 backup01:, then run ssh backup01 sha256sum -c webapp.tar.gz.sha256, which should print webapp.tar.gz: OK. Extract it on the other side into a new, empty directory and list it, all in one remote command: ssh backup01 "mkdir -p check && tar -xf webapp.tar.gz -C check && ls check/webapp". Finally, delete a local file and run rsync -avn --delete to read the deleting line before you decide whether the real run is what you want. To clean up, delete the backup01 block from ~/.ssh/config and run:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo userdel -r ca-backup rm -r ~/cmd-archive ~/.ssh/backup01_ed25519 ~/.ssh/backup01_ed25519.pub ssh-keygen -R 127.0.0.1
userdel: ca-backup mail spool (/var/mail/ca-backup) not found # Host 127.0.0.1 found: line 2 # Host 127.0.0.1 found: line 3 # Host 127.0.0.1 found: line 4 /home/deploy/.ssh/known_hosts updated. Original contents retained as /home/deploy/.ssh/known_hosts.old
# three known_hosts entries: after the first login, ssh also recorded the server's other host key types (UpdateHostKeys)

Takeaway

List an archive before you extract it, and extract it as yourself into an empty directory. Verify a download against a signed checksum before you unpack or run it, and dry-run every rsync that deletes.

Quick check
01You unpack a vendor's support bundle with sudo tar -xf bundle.tar.gz -C /opt/bundle and later find a set-user-ID root program inside it. Extracting the same archive as yourself would not have produced one. Why?
Incorrect — Compression knows nothing about permissions. tar records owners and modes in the archive, and gzip only shrinks the result.
Incorrect — -C only sets where extraction starts; member names stay relative to it, whoever runs tar.
Correct — For the superuser, tar defaults to keeping recorded ownership and permissions, so the archive decides who owns what.
Incorrect — GNU tar strips leading slashes on extraction for every user unless -P is given, and that has nothing to do with the mode bits.
02A nightly backup runs rsync -av /srv/app/ backup01:app/. On the second night it reports sent 245 bytes for a 7 GB directory, and the exit status is 0. What happened?
Correct — rsync's quick check compares size and modification time and sends nothing for files that match.
Incorrect — No compression option was used, and no compressor shrinks 7 GB of data to a few hundred bytes. The file list shows that nothing was sent.
Incorrect — A failed connection makes rsync print an error and exit with a non-zero status.
Incorrect — rsync also updates files that changed, as the edited webapp.conf showed. --delete only controls removing files that vanished from the source.
03A project's download page offers tool.tar.gz and tool.tar.gz.sha256 from the same server. sha256sum -c tool.tar.gz.sha256 prints OK. What has that proved?
Incorrect — Someone who controls the server can replace the archive and its checksum file together, and the check still passes.
Incorrect — A checksum says nothing about what the file does. It only compares bytes with a list.
Incorrect — No practical way is known to make a different file with the same SHA-256. The weakness is where the list came from, not the hash.
Correct — It proves the file is intact. Proving who made it needs a signature checked with a key you already trust, as gpgv does.

Related