Deployment guide

SFTP server

Videos

Overview: SFTP Server on Ubuntu 24.04 LTS on Azure: overview (watch on YouTube)

Launch and log in: SFTP Server on Ubuntu 24.04 LTS on Azure: launch and log in (watch on YouTube)

Deep Dive: SFTP Server on Ubuntu 24.04 LTS on Azure: deep dive (watch on YouTube)

Overview

This guide covers deploying and using SFTP Server 9.6 on Ubuntu 24.04 LTS from SecureAnyCloud on Microsoft Azure.

SFTP (the SSH File Transfer Protocol) moves files over an encrypted SSH connection on port 22. Every operating system has a client (sftp, WinSCP, FileZilla, Cyberduck) and so do most backup, ETL and B2B tools. This image is a plain, well-understood SFTP server: the OpenSSH server that ships with Ubuntu, set up so that you can hand accounts to partners and systems safely.

What you get:

  • SFTP users are jailed. Every member of the sftpusers group is locked (chrooted) into its own directory, /srv/sftp/<user>, on a dedicated 50 GiB Azure data disk. The jail is owned by root. The only place a user can write is its upload directory. SFTP users get no shell, no terminal and no port, agent, X11 or socket forwarding, so an SFTP account can move files and nothing else.
  • SFTP users are key-only by default. You can switch on password logins for SFTP users only with one command. The administrator account signs in the way you chose when you deployed the VM (SSH key or password), and root never logs in.
  • Passwords never go on a command line. The account tools either generate a password and show it once, or read it from standard input. A password typed as a command argument would end up in your shell history and in sudo's log, so the tools refuse that form.
  • Unique per VM. The image contains no SSH host key, no user and no key. On first boot each VM gets its own SSH host keys and a demo SFTP user, sftpdemo, with its own ed25519 key. The host key fingerprints are written to a root-only file, so you can check the very first connection.
  • Closed if anything goes wrong. First boot proves a full SFTP round trip, the jail and every refusal, and that no key leaked into a log, before it finishes. If any check fails, the demo user and its key are removed again.
  • Per-user quotas and an audit log that work. Quotas are enforced on the data disk at every boot, and a deleted user's limit is cleared so it never passes to a new account. Every SFTP session and file operation (open, read, write, rename, remove) is written to the system journal with the client's address. OpenSSH cannot log from inside a jail on its own, so the image places the journal's socket inside each jail for you.
  • Hardened service. fail2ban bans addresses that keep failing to log in. sshd, fail2ban and the boot preparation cannot reach the Azure Instance Metadata Service (169.254.169.254) or the Azure WireServer (168.63.129.16). The Azure Linux agent on the VM keeps working as normal.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
  • OpenSSH 9.6p1 from Ubuntu 24.04's own package (openssh-server 1:9.6p1-3ubuntu13), installed from the Ubuntu archive (noble-security), so Ubuntu keeps patching it and unattended-upgrades applies its fixes. "9.6" is the upstream release Ubuntu 24.04 maintains. This build carries every Ubuntu OpenSSH security update to date, including USN-6859-1 (CVE-2024-6387, "regreSSHion"), USN-6887-1 (CVE-2024-39894), USN-7270-1 (CVE-2025-26465, CVE-2025-26466), USN-7457-1 (CVE-2025-32728), USN-8090-1, USN-8222-1, USN-8533-1, USN-8721-1 and USN-8928-1 (October 2026). The exact package version is in Step 13.
  • fail2ban, the quota tools, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent. The SFTP account tools are in /usr/local/sbin (Step 8).

Default user: azureuser (or the administrator name you choose). Architecture: x64. Recommended size: Standard_B2s.

Prerequisites

Before you deploy this image, make sure you have:

  • An Azure subscription, an SSH key pair (or a password, if you choose password authentication for the administrator in the portal) and a virtual network in the target region.
  • A network security group that allows TCP 22 from your administrators' and your SFTP users' networks only. SFTP and administration share port 22. No other port is needed.
  • An SFTP client: the sftp command, WinSCP, FileZilla or Cyberduck.
1

Deploy the VM

In the Azure portal, choose Create a resource, search for SFTP Server on Ubuntu 24.04 LTS by SecureAnyCloud, and select Create.

Pick your subscription, resource group and region. Choose the Standard_B2s size (SFTP needs little CPU, so size for your users and transfer volume). Choose the administrator account, and use an SSH public key if you can.

Under Networking, allow inbound 22 only from the networks your users and administrators connect from.

The image brings its own 50 GiB data disk for /srv/sftp. You can enlarge it on the Disks tab, and first boot grows the file system to fill it.

To deploy with the Azure CLI, replace the values in angle brackets. The last command limits port 22 to one network:

az group create --name sftp-rg --location <region>
az vm image terms accept --urn sac1647283583153:sftp-server-ubuntu-24-04:default:latest
az vm create --resource-group sftp-rg --name sftp-server \
  --image sac1647283583153:sftp-server-ubuntu-24-04:default:latest \
  --size Standard_B2s --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_rsa.pub --public-ip-sku Standard
az network nsg rule update --resource-group sftp-rg --nsg-name sftp-serverNSG \
  --name default-allow-ssh --source-address-prefixes <your-client-cidr>
2

Connect to your VM

ssh azureuser@<vm-public-ip>

The administrator account works like any Ubuntu VM on Azure. Sign in over SSH with the key (or password) you chose at deployment, and use sudo for administration.

3

Check the services

First boot creates the demo user and proves an SFTP round trip before it finishes (a few seconds after SSH answers):

systemctl is-active ssh fail2ban sftp-prepare sftp-server-firstboot
sudo test -f /var/lib/sac/data/sftp-server-firstboot.done && echo FIRSTBOOT_DONE
active active active active FIRSTBOOT_DONE

sftp-server-firstboot runs once. After a later reboot it reports inactive, which is expected because the marker file stays in place.

Only sshd listens on a public address, on port 22:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0' | sort
0.0.0.0:22 users:(("sshd",...)) [::]:22 users:(("sshd",...))
4

Connection details and host key fingerprints

sudo grep -vE '^(#|$)' /root/sftp-server-info.txt

The file is readable by root only:

SFTP_HOST=<vm-public-ip> SFTP_PORT=22 SFTP_DEMO_USER=sftpdemo SFTP_DEMO_KEY=/root/sftp-demo-key SFTP_DEMO_KEY_FINGERPRINT=SHA256:<demo-key-fingerprint> SFTP_DEMO_JAIL=/srv/sftp/sftpdemo SFTP_PASSWORD_AUTH=off SFTP_ADMIN_USER=azureuser HOST_KEY_ED25519_SHA256=SHA256:<this-VM's-ed25519-host-key> HOST_KEY_ECDSA_SHA256=SHA256:<this-VM's-ecdsa-host-key> HOST_KEY_RSA_SHA256=SHA256:<this-VM's-rsa-host-key>

SFTP_HOST is the VM's public IP address from Azure instance metadata (the private address when the VM has no public IP). To print a DNS name there instead, put it in /etc/sac/sftp-hostname before first boot. First boot is the only part of the image that reads instance metadata, and it cannot reach the WireServer.

The first time any client connects, it shows the server's host key fingerprint and asks whether to trust it. It must equal one of the HOST_KEY_* values. If it does not, do not connect. You can print the fingerprints at any time:

for t in ed25519 ecdsa rsa; do ssh-keygen -l -E sha256 -f /etc/ssh/ssh_host_${t}_key.pub; done
5

Try the demo user

The demo user sftpdemo exists so you can try the server straight away. Its private key, unique to your VM, is /root/sftp-demo-key. Copy it to your own computer over your administrator SSH session, then connect:

ssh azureuser@<vm-public-ip> 'sudo cat /root/sftp-demo-key' > sftp-demo-key chmod 600 sftp-demo-key sftp -i sftp-demo-key sftpdemo@<vm-public-ip> sftp> cd upload sftp> put report.csv sftp> ls -l sftp> bye

The built-in check runs the same kind of session on the VM itself: an upload and a byte-for-byte download, the audit log entries, the jail, and the refusals (no shell, no terminal, no forwarding, no root login, no password while passwords are off):

sudo sftp-verify.sh | tail -7
ok TCP forwarding (-W 169.254.169.254:80) refused: administratively prohibited ok remote forwarding (-R) refused by the server (rc 255) ok root login refused (Permission denied) ok the demo key does not open the administrator account (azureuser) ok password authentication not offered to sftpdemo (key only: 'Permission denied (publickey)') SFTP_ROUNDTRIP_OK target 10.0.0.4

Once your own users exist, remove the demo user and its key with sudo sftp-deluser sftpdemo --yes && sudo shred -u /root/sftp-demo-key /root/sftp-demo-key.pub. After that, sftp-verify.sh has no account to test with.

6

Add a user with an SSH key (recommended)

Ask the user for their public key (the .pub file) and add it with --ssh-key @<file> (or paste the key line in quotes). --quota caps the user's storage. This example makes a key pair on the VM only to show the whole flow. In practice, the user keeps the private key and sends you the public one:

[ -f /tmp/bob_key ] || ssh-keygen -q -t ed25519 -N '' -C bob -f /tmp/bob_key
sudo sftp-adduser bob --ssh-key @/tmp/bob_key.pub --quota 10G
SFTP user "bob" ready. jail (chroot) : /srv/sftp/bob writable: /srv/sftp/bob/upload ssh key : added to /etc/ssh/authorized_keys.d/bob quota : 10G

The key is stored in /etc/ssh/authorized_keys.d/bob, owned by root and outside the jail, with OpenSSH's restrict option. A private key is refused if it is passed by mistake. Bob can now upload and download:

printf 'put /etc/hostname upload/hello.txt\nls -l upload\n' > /tmp/bob.batch sftp -i /tmp/bob_key -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/bob_known_hosts -b /tmp/bob.batch bob@127.0.0.1
sftp> put /etc/hostname upload/hello.txt sftp> ls -l upload -rw-r----- ? 1001 1001 12 Oct 25 10:10 upload/hello.txt

New files are created with mode 0640 (owner read-write, group read).

7

Password logins for SFTP users (optional)

Passwords are off for SFTP users on a fresh VM. Creating a user with --password generate switches password logins on for the sftpusers group only and prints a new 24-character password once, so store it straight away:

sudo sftp-adduser alice --password generate --quota 5G Note: SFTP password authentication is now ON for the sftpusers group (the administrator account is unaffected). SFTP user "alice" ready. jail (chroot) : /srv/sftp/alice writable: /srv/sftp/alice/upload quota : 5G GENERATED PASSWORD for alice (shown once, store it now): <24-character password>

To choose the password yourself, pass it on standard input and never on the command line. A command line ends up in your shell history and in sudo's log, so the tools refuse --password <value>:

read -rs PW && printf '%s\n' "$PW" | sudo sftp-adduser carol --password-stdin; unset PW

Check or change the setting at any time:

sudo sftp-password-auth status
SFTP password authentication: OFF (key only)

sudo sftp-password-auth on writes /etc/ssh/sshd_config.d/64-sac-sftp-password.conf (a Match Group sftpusers block with PasswordAuthentication yes). sudo sftp-password-auth off removes it. It never changes how the administrator signs in.

Moving a password user to keys: add their public key with sudo sftp-adduser <user> --ssh-key @<user>.pub, lock the password with sudo sftp-passwd <user> --lock (no password hash is kept), and once no one uses passwords, run sudo sftp-password-auth off.

fail2ban bans an address after 5 failed logins in 10 minutes (1 hour at first, growing to a week for repeat offenders).

8

Manage users

sudo sftp-listusers
USER PASSWORD KEYS QUOTA USED JAIL bob no 1 10240M 4.0K /srv/sftp/bob sftpdemo no 1 5120M 4.0K /srv/sftp/sftpdemo

Command Purpose

sftp-adduser <user> [--ssh-key @<file>] [--password generate \| --password-stdin] [--quota 10G] Create a user, or add a key, password or quota to one

sftp-passwd <user> generate \| --password-stdin \| --lock New password (printed once or read from standard input), or lock it

sftp-deluser <user> [--yes \| --keep-data] Remove a user and clear its quota limit; --yes also deletes /srv/sftp/<user>

sftp-listusers [--json] Users, password set, number of keys, quota, usage

sftp-password-auth on \| off \| status SFTP password logins for the sftpusers group

sftp-verify.sh, sftp-healthcheck The built-in end-to-end check and a quick health check

All of them need sudo. They refuse to touch an account that is not an SFTP user.

9

How the jail works

sudo ls -la /srv/sftp/bob
drwxr-xr-x 4 root root 4096 Oct 25 10:10 . drwxr-xr-x 5 root root 4096 Oct 25 10:10 .. drwxr-xr-x 2 root root 4096 Oct 25 10:10 dev drwxr-x--- 2 bob sftpusers 4096 Oct 25 10:10 upload

The jail root belongs to root (OpenSSH requires this), so the user cannot change it. upload is the user's own directory. dev holds only the socket that carries the audit log to the journal (Step 10). From the user's side, / is the jail, .. leads nowhere and system files do not exist:

printf 'pwd\ncd ..\npwd\nls /\n' > /tmp/bob.batch2 sftp -i /tmp/bob_key -o UserKnownHostsFile=/tmp/bob_known_hosts -b /tmp/bob.batch2 bob@127.0.0.1
sftp> pwd Remote working directory: / sftp> cd .. sftp> pwd Remote working directory: / sftp> ls / /dev /upload

An SFTP account cannot run commands either:

ssh -i /tmp/bob_key -o BatchMode=yes -o UserKnownHostsFile=/tmp/bob_known_hosts bob@127.0.0.1 id || true
This service allows sftp connections only.

The effective sshd settings for an SFTP user and for the administrator show the difference (replace azureuser with your administrator name if you chose another):

sudo sshd -T -C user=bob,host=client,addr=198.51.100.7 | grep -E '^(forcecommand|chrootdirectory|allowtcpforwarding|disableforwarding|permittty|authorizedkeysfile|passwordauthentication) '
sudo sshd -T -C user=azureuser,host=client,addr=198.51.100.7 | grep -E '^(permitrootlogin|forcecommand|chrootdirectory) '
passwordauthentication no permittty no allowtcpforwarding no disableforwarding yes forcecommand internal-sftp -u 0027 -l INFO chrootdirectory /srv/sftp/%u authorizedkeysfile /etc/ssh/authorized_keys.d/%u permitrootlogin no forcecommand none chrootdirectory none

The SFTP settings live in /etc/ssh/sshd_config.d/65-sac-sftp.conf and apply to members of the sftpusers group only. They keep SFTP users key-only even if you chose password sign-in for the administrator.

/etc/ssh/sshd_config.d/00-sac.conf refuses root logins, empty passwords and X11 forwarding for everyone. The administrator's password setting is the one you picked in the portal, and cloud-init writes it to 50-cloud-init.conf.

10

The transfer audit log

Every SFTP session and file operation is in the journal, with the client's address taken from the connection:

sudo journalctl -t internal-sftp --no-pager -n 8 -o cat
session opened for local user bob from [127.0.0.1] open "/upload/hello.txt" flags WRITE,CREATE,TRUNCATE mode 0644 close "/upload/hello.txt" bytes read 0 written 12 opendir "/upload/" closedir "/upload/" session closed for local user bob from [127.0.0.1]

A removal shows as remove name "/upload/<file>". OpenSSH writes these lines from inside the jail, where there is normally no log socket, so they would be lost. The image binds the journal's socket into each jail as dev/log at every boot (sftp-prepare.service) and whenever sftp-adduser creates a user. Logins and refusals are in sshd's own log: sudo journalctl -u ssh.

11

Security checks

fail2ban watches sshd's log and bans an address after repeated failed logins:

sudo fail2ban-client status sshd
Status for the jail: sshd |- Filter | |- Currently failed: 0 | |- Total failed: 0 | `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list:

To lift a ban, run sudo fail2ban-client set sshd unbanip <address>.

The image points fail2ban at ssh.service, the unit that sshd actually runs as. Ubuntu's stock filter watches sshd.service and would never ban anything.

sshd, fail2ban and the boot preparation are blocked from the Azure instance metadata service and the WireServer. First boot may read instance metadata (for the host address and the administrator name) but not the WireServer. SFTP users cannot reach either, because they cannot run commands or forward connections.

systemctl show -p IPAddressDeny ssh fail2ban sftp-prepare sftp-server-firstboot
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 IPAddressDeny=169.254.169.254/32 168.63.129.16/32 IPAddressDeny=169.254.169.254/32 168.63.129.16/32 IPAddressDeny=168.63.129.16/32

Each blocked unit checks this from the inside every time it starts:

sudo journalctl -u ssh -u fail2ban -u sftp-prepare -b -o cat --no-pager | grep -E '^METADATA_' | sort | uniq -c
3 METADATA_BLOCKED 168.63.129.16 3 METADATA_BLOCKED 169.254.169.254
12

The data disk and quotas

df -h /srv/sftp findmnt -no OPTIONS /srv/sftp
Filesystem Size Used Avail Use% Mounted on /dev/sdc 49G 60K 47G 1% /srv/sftp rw,nosuid,nodev,noexec,relatime,quota,usrquota,grpquota

/srv/sftp is its own Azure data disk (ext4, mounted by UUID with nodev,nosuid,noexec), so you can snapshot, resize or move it on its own.

To grow it, resize the disk in Azure. At the next boot, sftp-prepare.service grows the file system. Or run sudo resize2fs $(findmnt -no SOURCE /srv/sftp) straight away.

Quotas are enforced by the usrquota,grpquota mount options at every mount, so they survive reboots. An upload that would exceed a user's quota is refused with "Disk quota exceeded". Removing a user clears its limit, so a new account that later gets the same user ID starts without one.

To see each user's usage and limit (in KiB), run:

sudo repquota -u /srv/sftp | sed -n '1,3p;/bob\|sftpdemo/p'

The ext4 quota format needs the kernel's quota_v2 module. On the current Azure kernel it is part of the kernel's own modules package, which every kernel update installs together with the new kernel. On older Azure kernels, which kept it in linux-modules-extra, the image installs the matching metapackage instead. The file below records which applies to your VM:

modinfo -F filename quota_v2 cat /etc/sac/sftp-quota-kernel-meta
13

Updates

OpenSSH is Ubuntu's package, so apt and unattended-upgrades keep it patched along with the rest of the system. The installed version and where it came from:

dpkg-query -W openssh-server cat /usr/local/share/doc/sac-sftp/SOURCE.txt

After an OpenSSH update, sudo systemctl restart ssh makes the running server use it (open SFTP sessions are not interrupted).

14

Server components

Component Version Path

OpenSSH server (sshd, internal-sftp) 9.6p1, Ubuntu 1:9.6p1-3ubuntu13 (updated by apt) /usr/sbin/sshd

fail2ban Ubuntu 24.04 package /etc/fail2ban/jail.d/sac-sshd.conf

Ubuntu 24.04 LTS

Path Purpose

/etc/ssh/sshd_config.d/00-sac.conf No root login, no empty passwords, no X11 forwarding (for everyone)

/etc/ssh/sshd_config.d/64-sac-sftp-password.conf Present only while SFTP password logins are on

/etc/ssh/sshd_config.d/65-sac-sftp.conf The SFTP jail for the sftpusers group (key only by default)

/etc/ssh/authorized_keys.d/<user> SFTP users' public keys

/srv/sftp/<user> Each user's jail; upload/ is writable

/root/sftp-server-info.txt Connection details and host key fingerprints (mode 0600)

/root/sftp-demo-key The demo user's private key (mode 0600)

/usr/local/sbin/sftp-* Account tools, checks, the metadata probe and the per-boot preparation

/usr/local/share/doc/sac-sftp/SOURCE.txt Version and source notes

Troubleshooting

  • "Permission denied (publickey)" for an SFTP user. The key is not in /etc/ssh/authorized_keys.d/<user>, or the user is using the wrong private key. Add the public key with sftp-adduser <user> --ssh-key @<file>. Password users need sftp-password-auth on (Step 7).
  • "Connection refused" or a timeout after several failed logins. fail2ban has banned the address. Check sudo fail2ban-client status sshd and unban it (Step 11). Also check that the network security group allows TCP 22 from that address.
  • "Couldn't write to remote file ... Permission denied". The user tried to write outside upload/. The jail root is read-only by design.
  • "Disk quota exceeded". Raise the quota with sudo sftp-adduser <user> --quota <size> or remove files.
  • The client warns that the host key changed. A new VM always has new host keys. Compare the fingerprint with /root/sftp-server-info.txt before accepting it.
  • Nothing works after deployment. Check systemctl status sftp-server-firstboot and journalctl -u sftp-server-firstboot.

Security recommendations

  • Allow TCP 22 in the network security group only from the networks your users and administrators use.
  • Prefer SSH keys. If you turn passwords on, use the generated passwords, and turn them off again when you no longer need them.
  • Remove the demo user and its key once your own users exist (Step 5).
  • Keep unattended upgrades on, and restart sshd after OpenSSH updates.
  • Send the journal (journalctl -t internal-sftp, journalctl -u ssh) to your log service, for example Azure Monitor, if you need to keep an audit trail beyond the VM.

Support and licensing

SecureAnyCloud provides support for this image by email and chat, around the clock. OpenSSH is free software under the OpenSSH license (the original author's free-use grant, with parts under the BSD and ISC licenses). The full terms are in /usr/share/doc/openssh-server/copyright.

OpenSSH is a trademark of the OpenBSD project. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the OpenBSD or OpenSSH projects; the name only identifies the open source software in this image.

Deploy on Azure

Launch SFTP Server on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure