Deployment guide

ProFTPD 1.3.9e

Overview

This guide shows you how to launch and use the ProFTPD 1.3.9e on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The image is one version of the SecureAnyCloud File Sharing and Sync listing on AWS Marketplace. You choose it as the software version when you subscribe and launch.

ProFTPD is a mature, highly configurable FTP server. Use it when your partners, devices or older systems speak FTP. Typical cases are scanners and cameras that upload over FTP, line-of-business systems that drop files every night, and people who use FileZilla, WinSCP or Cyberduck. The SecureAnyCloud image makes the server safe to put on a network. What you get:

  • FTPS only. Explicit FTP over TLS (AUTH TLS on port 21) is required for the login and for every transfer and directory listing. A client that tries plain FTP is refused before it can send a password. A client that encrypts the login but not the data is refused too. Only TLS 1.2 and TLS 1.3 are accepted.
  • A user and certificate unique to your instance. First boot creates the FTP user SecureAnyCloud with a random 24-character password and a TLS certificate for your instance. It writes them to /root/proftpd-credentials.txt (readable by root only). ProFTPD stores only a SHA-512-crypt hash of the password. No password or key is baked into the image.
  • No anonymous and no system accounts. Only the virtual users in ProFTPD's own user file can log in, and only members of the sacftp group. The Linux accounts on the instance, including root and ubuntu, cannot.
  • Everyone in a jail. Each user is locked (chrooted) into its own directory, /srv/ftp/<user>, on a dedicated 20 GiB EBS data volume, and works as the unprivileged ftpvirtual account. No ProFTPD process runs as root. The listening process runs as the proftpd account with only the Linux capabilities it needs.
  • Automatic bans and slow guessing. mod_ban bans a client address that keeps failing to log in for an hour, and it bans connection floods for 15 minutes. mod_delay makes failed logins take as long as successful ones.
  • A smaller server. Only the modules an FTPS server needs are compiled in. mod_copy (the SITE CPFR/SITE CPTO commands behind several past ProFTPD vulnerabilities) and mod_sftp are not compiled. Active mode (PORT/EPRT) is refused, and the welcome banner does not reveal the software or version.
  • Closed until first boot, and fail closed. The image holds no user file and no certificate key, so the server cannot start. First boot creates both and starts the server. It proves a full encrypted round trip and that no password leaked into a log, and only then finishes. If anything fails, it stops the server again.
  • Hardened service. The server runs in a systemd sandbox that can write only to /srv/ftp, its logs and its runtime directory. It is blocked from the instance metadata service, and the AMI requires IMDSv2.
  • Fully patched base. Ubuntu 24.04 LTS is fully patched at build time, with unattended security upgrades switched on.

What is on the image:

  • ProFTPD 1.3.9e, built by SecureAnyCloud from the signed upstream source and not installed from apt. Ubuntu 24.04 ships ProFTPD 1.3.8b in its community-maintained (universe) section. The image builds the upstream 1.3.9e release from ftp://ftp.proftpd.org/distrib/source/proftpd-1.3.9e.tar.gz. Before anything was compiled, the build verified both the tarball's SHA-256 checksum and its detached signature from the ProFTPD maintainer's key. It is built against Ubuntu's OpenSSL 3, the build removed the compiler afterwards, and the complete corresponding source ships on the image (Step 14).
  • One SecureAnyCloud change to the upstream source, disclosed in full in Step 14: the ban table's shared memory is created readable and writable by root only.
  • Updates: because ProFTPD is built from source, apt upgrade and unattended-upgrades do not patch ProFTPD itself. The operating system, OpenSSL (which provides the TLS) and every other package still update through apt. When upstream releases a ProFTPD fix, SecureAnyCloud publishes a new image version on this listing (Step 15).
  • Ubuntu 24.04 LTS, OpenSSL 3, cloud-init and the AWS Systems Manager agent. The account and maintenance tools are in /usr/local/sbin (Step 10).

Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.

Prerequisites

  • An AWS account that is subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows the following, from your own networks and your FTP users' addresses only: TCP 22 for SSH administration, TCP 21 for the FTPS control connection, and TCP 40000-40099 for passive data connections (the exact range the server uses).
  • An FTPS client: FileZilla, WinSCP, Cyberduck, lftp or curl.

Do not open ports 21 and 40000-40099 to 0.0.0.0/0 unless the server must accept uploads from anywhere on the internet. If you do, expect constant password guessing from scanners. The automatic bans and the per-instance password keep the scanners out, but every attempt still costs you log noise and connections. List the address ranges your users and systems connect from instead.

1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick ProFTPD 1.3.9e on Ubuntu 24.04 as the software version, choose your Region, and then choose Launch through EC2. Choose m5.large (FTP needs little CPU, so size for your users and transfer volume), your key pair, and a security group as described above. The AMI carries two volumes: the 8 GiB root volume and a 20 GiB data volume for /srv/ftp. You can make the data volume larger at launch. Every boot grows the file system to fill it.

To launch with the AWS CLI, replace the values in angle brackets. The block device mapping sets the data volume size:

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --block-device-mappings 'DeviceName=/dev/sdf,Ebs={VolumeSize=100,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=proftpd}]'

To add the FTPS rules to an existing security group for one client network, run the following command. It uses the documentation range 203.0.113.0/24, so substitute your own range:

aws ec2 authorize-security-group-ingress --group-id <your-security-group> \
  --ip-permissions 'IpProtocol=tcp,FromPort=21,ToPort=21,IpRanges=[{CidrIp=203.0.113.0/24}]' \
  'IpProtocol=tcp,FromPort=40000,ToPort=40099,IpRanges=[{CidrIp=203.0.113.0/24}]'
2

Connect to your instance

Version SSH user

ProFTPD 1.3.9e on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>

Port 22 is for you, the administrator, and you sign in with your EC2 key pair (password logins are off). It is not an SFTP service for your FTP users. Their accounts exist only inside ProFTPD, so they cannot use SSH or SFTP at all, and this image has no SFTP server for them (mod_sftp is not compiled). If your users need SFTP rather than FTPS, choose an SFTP server version from the same listing.

3

Check the services

First boot creates the user and the certificate, starts the server and proves an encrypted round trip before it finishes. This usually takes under a minute. Run:

systemctl is-active proftpd proftpd-prepare proftpd-firstboot
sudo test -f /var/lib/sac/data/proftpd-firstboot.done && echo FIRSTBOOT_DONE
active active active FIRSTBOOT_DONE

Only SSH and ProFTPD listen on public addresses. ProFTPD listens on IPv4 only, and the passive data ports open only for the duration of a transfer. Confirm this with:

sudo ss -lntpH | awk '{split($6,p,"\""); print $4, p[2]}' | grep -v '127.0.0' | sort
0.0.0.0:21 proftpd 0.0.0.0:22 sshd [::]:22 sshd
4

Get your user and password

The credentials file is readable by root only. Run the following to show everything in it except the password:

sudo grep -vE '^(#|$|FTP_PASSWORD=)' /root/proftpd-credentials.txt
sudo stat -c '%a %U %n' /root/proftpd-credentials.txt
FTP_HOST=ec2-<public-ip-dashed>.compute-1.amazonaws.com FTP_PORT=21 FTP_USER=sac FTP_TLS=explicit FTPS (AUTH TLS) required for login and data FTP_PASSIVE_PORTS=40000-40099 FTP_PASSIVE_ADDRESS=<public-ip> FTP_HOME=/srv/ftp/sac TLS_CERT_SHA256=<certificate-sha256-fingerprint> 600 root /root/proftpd-credentials.txt

To print the password itself, run the following command where nobody can read your screen. Do not paste the password into tickets or chat.

sudo grep '^FTP_PASSWORD' /root/proftpd-credentials.txt | cut -d= -f2-

FTP_HOST is the instance's public DNS name from EC2 instance metadata. When the subnet has no public DNS names, it is the public or private IP address instead. The certificate also names the public and private IP addresses, so connecting by IP address works too. FTP_PASSIVE_ADDRESS is the address the server announces for passive transfers (Step 11). To use your own DNS name, see Step 12.

5

Connect with an FTPS client

In FileZilla, open File > Site Manager > New site and enter these settings:

Field Value

Protocol FTP - File Transfer Protocol

Host FTP_HOST from Step 4, port 21

Encryption Require explicit FTP over TLS

Logon type Normal (or Ask for password)

User / Password SecureAnyCloud and the password from Step 4

On the Transfer Settings tab, keep Passive mode, because the server refuses active mode. The first time you connect, FileZilla shows the server's certificate. It is self-signed and was created on your instance. Compare its SHA-256 fingerprint with TLS_CERT_SHA256 from Step 4 before you select Always trust this certificate. Do not accept a certificate whose fingerprint does not match. You can print the fingerprint on the instance at any time:

sudo openssl x509 -noout -subject -fingerprint -sha256 -ext subjectAltName -in /etc/proftpd/tls/proftpd.crt
subject=CN = ec2-<public-ip-dashed>.compute-1.amazonaws.com, O = sac sha256 Fingerprint=<certificate-sha256-fingerprint> X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1

WinSCP: File protocol FTP, Encryption TLS/SSL Explicit encryption. Cyberduck: FTP-SSL (Explicit AUTH TLS).

From a terminal on your own computer, first save the server's certificate and check its fingerprint against TLS_CERT_SHA256. Then let curl verify the server with it. The --ssl-reqd option makes curl refuse to continue without TLS, and curl asks for the password when you give only the user name:

openssl s_client -starttls ftp -connect <FTP_HOST>:21 </dev/null 2>/dev/null | openssl x509 > proftpd.crt
openssl x509 -noout -subject -fingerprint -sha256 -in proftpd.crt curl --ssl-reqd --cacert proftpd.crt -u sac -T ./report.txt ftp://<FTP_HOST>/report.txt curl --ssl-reqd --cacert proftpd.crt -u sac --list-only ftp://<FTP_HOST>/ curl --ssl-reqd --cacert proftpd.crt -u sac ftp://<FTP_HOST>/report.txt curl --ssl-reqd --cacert proftpd.crt -u sac -Q 'DELE report.txt' -o /dev/null ftp://<FTP_HOST>/

Run from a laptop outside AWS against a test instance, the commands printed the listing and the downloaded file:

report.txt hello from my laptop

With lftp, force TLS on the login and the data, and trust the saved certificate:

lftp -u sac <FTP_HOST> lftp sac@<FTP_HOST>:~> set ftp:ssl-force true lftp sac@<FTP_HOST>:~> set ftp:ssl-protect-data true lftp sac@<FTP_HOST>:~> set ssl:ca-file proftpd.crt lftp sac@<FTP_HOST>:~> put lftp.txt lftp sac@<FTP_HOST>:~> ls
-rw-r--r-- 1 <user> sacftp 16 Oct 31 02:43 lftp.txt

The server requires the client to reuse the control connection's TLS session on every data connection, so nobody can hijack a transfer. FileZilla, WinSCP, lftp and curl built with OpenSSL or GnuTLS do this. The curl that ships with macOS (LibreSSL) cannot resume a TLS 1.3 session and fails transfers with 425 Unable to build data connection: Operation not permitted. Add --tls-max 1.2 to its commands, or use a curl built with OpenSSL, such as the one from Homebrew.

6

Upload and download from the instance

The following command uploads a file, reads it back, lists the directory and deletes the file. It does all of this over FTPS to the instance's own private address, and it checks the server certificate. The password is passed to curl on standard input, so it never appears in the process list, your shell history or the output:

sudo bash -c ' F=/root/proftpd-credentials.txt U=$(grep "^FTP_USER" $F | cut -d= -f2-); P=$(grep "^FTP_PASSWORD" $F | cut -d= -f2-) IP=$(hostname -I | cut -d" " -f1) ftps() { printf "user = \"%s:%s\"\n" "$U" "$P" | curl -K - -sS --ssl-reqd --cacert /etc/proftpd/tls/proftpd.crt "$@"; } echo "hello from sac" > /run/report.txt ftps -T /run/report.txt "ftp://$IP/report.txt" && echo "upload -> ok" echo "download -> $(ftps "ftp://$IP/report.txt")" echo "list -> $(ftps --list-only "ftp://$IP/" | tr "\n" " ")" ftps -Q "DELE report.txt" -o /dev/null "ftp://$IP/" && echo "delete -> ok" rm -f /run/report.txt '
upload -> ok download -> hello from sac list -> report.txt delete -> ok

Uploaded files land in the user's jail, /srv/ftp/sac, owned by the unprivileged ftpvirtual account. To drop files in for a user over SSH, copy them there and give them to that account with sudo chown ftpvirtual:sacftp /srv/ftp/sac/<file>.

7

Run the built-in check

The image ships an end-to-end check that you can run at any time. First boot runs it too. The check reads the credentials file itself, passes the password to curl in a root-only temporary file, and drives the server over FTPS through the instance's private address:

sudo proftpd-verify.sh
ok banner '220 FTP server ready' (no software/version) ok plain FTP login refused before PASS (550, curl rc 67) ok anon login refused over FTPS (530) ok ftp login refused over FTPS (530) ok wrong password refused (530) ok cleartext data channel refused (curl rc 28) ok server certificate is this instance's (<fingerprint>...) ok tls1_2 accepted ok tls1_3 accepted ok tls1 refused ok tls1_1 refused ok upload (EPSV) sac-verify-18711.bin ok download (PASV) byte-identical ok passive data port 40001 inside 40000-40099 ok PASV reply carries the masquerade address <public-ip> ok data channel protected (PROT P accepted) ok listing shows the upload ok delete ok file gone after delete ok file gone from /srv/ftp/sac ok CWD .. / ../.. stay at the jail root ok ../../../etc/passwd unreachable (curl rc 9) ok /etc/passwd (absolute) unreachable (curl rc 9) ok /proc/self/root/etc/passwd unreachable (CVE-2026-35025 class) ok root listing holds only the user's own files ok SITE CPFR refused (mod_copy absent) ok active mode (PORT and EPRT) refused ok live session process runs as ftpvirtual ok no root proftpd process: the listening master runs as the unprivileged proftpd account (mod_cap) PROFTPD_ROUNDTRIP_OK target <private-ip>

The check takes about half a minute, because it tries one wrong password on purpose and mod_delay makes it wait. It stays well below the ban threshold and leaves the user's directory as it found it.

8

Security checks

A client that does not start TLS gets the welcome banner, which names no software or version. The server then refuses the client at the user name, before it can send a password:

IP=$(hostname -I | cut -d" " -f1); (curl -sv -m 8 "ftp://$IP/" 2>&1 || true) | grep -E '^< [0-9]{3}' | tr -d '\r'
< 220 FTP server ready < 550 SSL/TLS required on the control channel

Only the modules an FTPS server needs are compiled in. There is no mod_copy, no mod_sftp, no mod_sql and no loadable module support. Verify this with:

/usr/local/sbin/proftpd -v /usr/local/sbin/proftpd -l | tr -d ' ' | sed 1d | tr '\n' ' '; echo
ProFTPD Version 1.3.9e mod_core.c mod_xfer.c mod_rlimit.c mod_auth_unix.c mod_auth_file.c mod_auth.c mod_ls.c mod_log.c mod_site.c mod_delay.c mod_facts.c mod_tls.c mod_ban.c mod_cap.c mod_ctrls.c

The configuration requires TLS for logins and data and allows only TLS 1.2 and 1.3. It uses only ProFTPD's own user file, lets only the sacftp group log in, and jails everyone. It also refuses active mode and the SITE copy commands, and it bans repeat offenders:

grep -E '^(ServerIdent|AuthOrder|RootLogin|DefaultRoot|MaxLoginAttempts|PassivePorts|Include|TLSEngine|TLSRequired|TLSProtocol|BanOnEvent|DelayEngine|UseIPv6) ' /etc/proftpd/proftpd.conf
grep -A2 -E '^<Limit (LOGIN|EPRT PORT|SITE_CPFR)' /etc/proftpd/proftpd.conf
ServerIdent on "FTP server ready" UseIPv6 off MaxLoginAttempts 3 AuthOrder mod_auth_file.c RootLogin off DefaultRoot ~ PassivePorts 40000 40099 Include /etc/proftpd/sac-masquerade.conf TLSEngine on TLSRequired on TLSProtocol TLSv1.2 TLSv1.3 BanOnEvent MaxLoginAttempts 2/00:10:00 01:00:00 BanOnEvent ClientConnectRate 150/00:01:00 00:15:00 DelayEngine on <Limit LOGIN> AllowGroup sacftp DenyAll -- <Limit EPRT PORT> DenyAll </Limit> -- <Limit SITE_CPFR SITE_CPTO SITE_COPY> DenyAll </Limit>

The files that matter are locked down, the user file holds only a SHA-512-crypt hash, and the FTP user is not a Linux account. Check this with:

sudo stat -c '%a %U:%G %n' /etc/proftpd/ftpd.passwd /etc/proftpd/ftpd.group /etc/proftpd/tls/proftpd.key /root/proftpd-credentials.txt /srv/ftp /srv/ftp/sac
sudo cut -d: -f1,6,7 /etc/proftpd/ftpd.passwd;
sudo cut -d: -f2 /etc/proftpd/ftpd.passwd | cut -c1-14 getent passwd sac || echo "sac: no system account"
600 root:root /etc/proftpd/ftpd.passwd 600 root:root /etc/proftpd/ftpd.group 600 root:root /etc/proftpd/tls/proftpd.key 600 root:root /root/proftpd-credentials.txt 755 root:root /srv/ftp 750 ftpvirtual:sacftp /srv/ftp/sac sac:/srv/ftp/sac:/usr/sbin/nologin $6$rounds=1000 sac: no system account

No ProFTPD process runs as root. The server runs in a sandbox that can write only to the data volume, its logs and its runtime directory, and it cannot reach the instance metadata service:

ps -o user=,args= -C proftpd systemctl show proftpd -p NoNewPrivileges -p ProtectSystem -p ReadWritePaths -p IPAddressDeny cat /etc/systemd/system/proftpd.service.d/50-sac-imds-deny.conf
proftpd proftpd: (accepting connections) IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128 ReadWritePaths=/srv/ftp /var/log/proftpd /run/proftpd ProtectSystem=strict NoNewPrivileges=yes
# sac: the FTP server never needs the instance metadata service (or the Azure wire server); deny both. [Service] IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

Confirm that no per-instance password reached a log, the journal, shell history, backups, /etc or the data volume:

sudo proftpd-secret-leak-check.sh
LEAKCHECK_OK no per-instance password in the journal, auth.log, syslog, cloud-init, the ProFTPD logs, shell history, MOTD, /var/backups, /etc, /srv/ftp or /tmp

ProFTPD logs the address each client connected from, and never a forwarded header. The journal and the transfer log therefore show who did what:

sudo journalctl -u proftpd --no-pager -o cat | grep -E 'FTP session opened|Login successful' | tail -n 2
sudo tail -n 2 /var/log/proftpd/xferlog
2026-10-31 02:50:10,338 ip-<private-ip-dashed> proftpd[18825] <private-ip> (<private-ip>[<private-ip>]): USER sac: Login successful. 2026-10-31 02:50:12,669 ip-<private-ip-dashed> proftpd[18899] <private-ip> (<private-ip>[<private-ip>]): FTP session opened. Sat Oct 31 02:50:10 2026 1 <private-ip> 131072 /srv/ftp/sac/sac-verify-live-18711.bin b _ i r sac ftps 0 * c Sat Oct 31 02:50:10 2026 0 <private-ip> 131072 /srv/ftp/sac/sac-verify-live-18711.bin a _ d r sac ftps 0 * c
9

Automatic bans

ProFTPD closes a session after 3 failed logins (MaxLoginAttempts 3). With BanOnEvent MaxLoginAttempts 2/00:10:00 01:00:00, a client address that does that twice within 10 minutes is banned for one hour, and BanOnEvent ClientConnectRate 150/00:01:00 00:15:00 bans an address that opens more than 150 connections a minute for 15 minutes. mod_delay (DelayEngine on) evens out the time a failed and a successful login take, so response times do not reveal which user names exist.

Show the current bans and the ban table's shared memory segment (owner root, permissions 600; see Step 14):

sudo ftpdctl ban info
sudo ipcs -m | grep -E 'perms|root'
ftpdctl: No bans key shmid owner perms bytes nattch status 0x4c1d1770 4 root 600 371680 1

The following output shows what a ban looks like. On a test instance, a client failed three logins in each of two sessions within a minute. The second session ended with the ban, and ftpdctl ban info -v then showed the banned address, the reason and the expiry:

331 Password required for nosuchuser 530 Login incorrect. 331 Password required for nosuchuser 530 Login incorrect. 331 Password required for nosuchuser 530 Login incorrect. 530 Host <client-ip> has been banned
sudo ftpdctl ban info -v
ftpdctl: Banned Hosts: ftpdctl: <client-ip> ftpdctl: Reason: MaxLoginAttempts autoban at Sat Oct 31 02:40:23 2026 ftpdctl: Expires: Sat Oct 31 03:40:23 2026 (in 3576 seconds) ftpdctl: <VirtualHost>: sac FTPS server (0.0.0.0#21)

If you ban one of your own systems by mistake, such as a scanner with an old password, lift the ban for its address and then fix the password on that system:

sudo ftpdctl permit host <client-ip>
ftpdctl: host '<client-ip>' permitted

Bans live in memory and end on their own. A restart of the server clears them.

10

Users and passwords

Every user gets its own jail, /srv/ftp/<user>, and never sees the others. To add a user with a generated password, run the following command. The password is shown once:

sudo proftpd-adduser alice

Alternatively, set the password yourself on standard input, so it never appears on a command line or in your history. Use at least 12 characters:

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

To change a password, use a generated one or --password-stdin. For the SecureAnyCloud user, this also updates /root/proftpd-credentials.txt. ProFTPD reads the user file at every login, so the change applies to the next login without a restart:

sudo proftpd-passwd sac

To delete a user, run the following command. Add --remove-files to delete the user's directory too. It removes only a real directory owned by ftpvirtual directly under /srv/ftp, never a link:

sudo proftpd-deluser alice --remove-files

To list the users and their jails, run:

sudo proftpd-listusers
sac /srv/ftp/sac
11

Passive mode, the passive address and the firewall

FTP uses a second connection for every transfer and listing. This server supports passive mode only. The client opens that connection to a port that the server announces from 40000-40099, so your security group must allow that range as well as port 21, from the same client addresses. The server refuses active mode, where it would connect back to the client (PORT/EPRT), so it never opens outbound connections to addresses a client names. The server allows up to 50 simultaneous sessions and at most 8 from one address.

Clients that use the EPSV command (FileZilla, WinSCP, curl, lftp and most others) reuse the address they connected to and need only the port. Older clients use PASV, where the server also announces an address (MasqueradeAddress). A fresh instance knows only its private address, so on every boot proftpd-prepare (a oneshot service that runs before ProFTPD) chooses the address:

  • If /etc/sac/proftpd-masquerade exists, the service uses its first line: an IPv4 address, a DNS name, or none.
  • Otherwise, it uses the instance's public IPv4 address from instance metadata (IMDSv2). This address can change when you stop and start the instance, which is why the service runs at every boot.
  • With neither (a private-only instance), no MasqueradeAddress is written and PASV announces the address the client connected to.
cat /etc/proftpd/sac-masquerade.conf
# Written by proftpd-prepare at every boot - do not edit. Set your own passive address in /etc/sac/proftpd-masquerade
# (an IPv4 address, a DNS name, or "none"), then:
sudo proftpd-prepare --restart
# source: EC2 IMDSv2 public-ipv4 MasqueradeAddress <public-ip>

A PASV session from a laptop outside AWS, with curl told to use the announced address, shows the public address and a port inside the range (156*256+70 = 40006):

curl -v --ssl-reqd --cacert proftpd.crt -u sac --disable-epsv --no-ftp-skip-pasv-ip --list-only ftp://<FTP_HOST>/
> PROT P < 200 Protection set to Private < 257 "/" is the current directory > PASV < 227 Entering Passive Mode (<public-ip-commas>,156,70). * Connecting to <public-ip> (<public-ip>) port 40006 < 200 Type set to A > NLST < 150 Opening ASCII mode data connection for file list < 226 Transfer complete

Elastic IP, DNS name or private only. Behind an Elastic IP, a Network Load Balancer or your own DNS name, write that address or name to /etc/sac/proftpd-masquerade. Write none when every client is inside the VPC or connects over a VPN or Direct Connect. Then apply the change. This rewrites the generated file and restarts ProFTPD, which ends open sessions:

echo 198.51.100.25 | sudo tee /etc/sac/proftpd-masquerade
sudo proftpd-prepare --restart

To go back to the instance's public address, delete the file and run sudo proftpd-prepare --restart again. Do not edit /etc/proftpd/sac-masquerade.conf itself, because the server rewrites it at every boot.

If you change PassivePorts in /etc/proftpd/proftpd.conf, change the security group to the same range.

12

Your own DNS name and certificate

SecureAnyCloud only ever replaces its own certificate. When first boot creates the instance's self-signed certificate, it records the certificate's SHA-256 fingerprint in the root-owned file /var/lib/sac/data/proftpd-tls-sac.sha256. The certificate is re-created only when the installed certificate still matches that fingerprint. A certificate you install at /etc/proftpd/tls/proftpd.crt no longer matches, so it is never replaced: not at boot, not by first boot, and not when you change the host name. Check which certificate is installed:

CUR=$(sudo openssl x509 -noout -fingerprint -sha256 -in /etc/proftpd/tls/proftpd.crt | cut -d= -f2) if [ "$CUR" = "$(cat /var/lib/sac/data/proftpd-tls-sac.sha256)" ]; then echo "sac self signed certificate $CUR"; else echo "your own certificate $CUR"; fi
sac self signed certificate <certificate-sha256-fingerprint>

A new name for the self-signed certificate. Point a DNS record at the instance (or attach an Elastic IP), then set the name the server presents. While SecureAnyCloud's certificate is installed, this creates a new self-signed certificate for the name and the instance's current addresses. It records the fingerprint, updates FTP_HOST and TLS_CERT_SHA256 in /root/proftpd-credentials.txt, restarts the server and keeps the name for later runs. Run the command without a name after a stop and start has given the instance new addresses:

sudo proftpd-set-hostname ftp.example.com

Your own certificate, for example from Let's Encrypt. FTPS clients cannot use the HTTP challenge, so use a DNS challenge. This example assumes the domain is in Route 53 and the instance role is allowed to change that zone. The Certbot Route 53 plugin runs outside the FTP server, which is the only service blocked from instance metadata:

sudo apt-get install -y certbot python3-certbot-dns-route53
sudo certbot certonly --dns-route53 -d ftp.example.com

Install the certificate, its key and the intermediate chain at the documented paths. Tell ProFTPD about the chain once, check the configuration and restart:

sudo install -o root -g root -m 0644 /etc/letsencrypt/live/ftp.example.com/cert.pem /etc/proftpd/tls/proftpd.crt
sudo install -o root -g root -m 0600 /etc/letsencrypt/live/ftp.example.com/privkey.pem /etc/proftpd/tls/proftpd.key
sudo install -o root -g root -m 0644 /etc/letsencrypt/live/ftp.example.com/chain.pem /etc/proftpd/tls/chain.pem grep -q '^TLSCertificateChainFile' /etc/proftpd/proftpd.conf || echo 'TLSCertificateChainFile /etc/proftpd/tls/chain.pem' | sudo tee -a /etc/proftpd/proftpd.conf
sudo /usr/local/sbin/proftpd -t && sudo systemctl restart proftpd

Put the last four lines in a Certbot deploy hook (/etc/letsencrypt/renewal-hooks/deploy/proftpd) so renewals are installed too, and set the MasqueradeAddress to the same name (Step 11). Your clients then trust the certificate without a fingerprint check. You install a certificate from your own certificate authority the same way.

Going back to a self-signed certificate. proftpd-set-hostname refuses to touch a certificate that SecureAnyCloud did not create (the installed certificate was not minted by SecureAnyCloud (yours?) - not replacing it). To replace yours with a new SecureAnyCloud self-signed certificate, say so explicitly. Remove the TLSCertificateChainFile line first if you added one:

sudo proftpd-set-hostname --replace-own-cert
13

The data volume

/srv/ftp is its own EBS volume. It is mounted by file system UUID with nodev,nosuid,noexec, so uploads can never be run as programs:

findmnt -no SOURCE,FSTYPE,OPTIONS /srv/ftp df -h --output=size,used,avail,target /srv/ftp
/dev/nvme1n1 ext4 rw,nosuid,nodev,noexec,relatime Size Used Avail Mounted on 20G 28K 19G /srv/ftp

To grow the volume, increase its size in the EC2 console (Volumes > Modify volume) and wait until the modification reaches the optimizing state. Then grow the file system online. The next boot does this too:

sudo proftpd-prepare

Back up the volume with EBS snapshots or AWS Backup. A snapshot of the data volume holds every user's files. Also back up /etc/proftpd/ftpd.passwd and /etc/proftpd/ftpd.group, which hold your users.

14

Source code, the SecureAnyCloud change and the license

ProFTPD is licensed under the GNU General Public License, version 2 or later. The ProFTPD on this image is built from the signed upstream 1.3.9e tarball with one SecureAnyCloud change. In contrib/mod_ban.c, line 835, the System V shared memory segment that holds the ban table is created with mode 0600 instead of 0666.

With 0666, any local account on the instance could read and write the ban table, which the ProFTPD server process trusts. A local user could lift bans or ban addresses. This is upstream Issue #2388. The 1.3.9e release notes (NEWS) list it as fixed, but the 1.3.9e source still creates the segment with 0666, so SecureAnyCloud applies the one-line change itself. On a running instance, the result is visible as a 600 root segment in sudo ipcs -m.

The exact change ships on the image:

cat /usr/local/share/doc/proftpd/proftpd-1.3.9e-mod_ban-shm-0600.patch
--- a/contrib/mod_ban.c +++ b/contrib/mod_ban.c @@ -832,7 +832,7 @@ */ PRIVS_ROOT - shmid = shmget(key, sizeof(struct ban_data), IPC_CREAT|IPC_EXCL|0666); + shmid = shmget(key, sizeof(struct ban_data), IPC_CREAT|IPC_EXCL|0600); xerrno = errno; PRIVS_RELINQUISH

SOURCE.txt records where the source came from, how it was verified and built, the change, and the written offer:

cat /usr/local/share/doc/proftpd/SOURCE.txt
This ProFTPD 1.3.9e was built by sac from the upstream release tarball proftpd-1.3.9e.tar.gz (ftp://ftp.proftpd.org/distrib/source/, git tag v1.3.9e = commit fc3f908a0d07fa3b17fbe1e3054b7520806bc75a), whose SHA-256 checksum 7db91f229f8c3ad18e233f38a3c1749bd11bf92e1079b6c7f86e51a3fcfce5eb and detached signature (key 697E684D1668D6968428405CB78E893FA511976A, signed 2026-10-26) were both verified before the build, configured with: --prefix=/usr/local --sysconfdir=/etc/proftpd --localstatedir=/run/proftpd --with-modules=mod_tls:mod_ban --enable-openssl --enable-ctrls --enable-cap --disable-auth-pam --disable-ident --disable-dso --disable-nls --disable-sodium --disable-memcache --disable-redis --disable-pcre --disable-pcre2 --disable-facl --disable-curses --disable-ncurses. One sac change to the verified source: in contrib/mod_ban.c, line 835, the call shmget(key, sizeof(struct ban_data), IPC_CREAT|IPC_EXCL|0666) is changed to use mode 0600 instead of 0666, because with 0666 any local account could read and write the ban table that the root master process trusts (upstream issue 2388, listed as fixed in the 1.3.9e NEWS but still present in the 1.3.9e tarball). The exact change is the unified diff shipped at /usr/local/share/doc/proftpd/proftpd-1.3.9e-mod_ban-shm-0600.patch; to obtain the corresponding source, download the unmodified signed upstream tarball proftpd-1.3.9e.tar.gz (SHA-256 7db91f229f8c3ad18e233f38a3c1749bd11bf92e1079b6c7f86e51a3fcfce5eb, signing key 697E684D1668D6968428405CB78E893FA511976A; its checksum and detached signature are published at https://github.com/proftpd/proftpd/releases/tag/v1.3.9e and the tarball itself at ftp://ftp.proftpd.org/distrib/source/proftpd-1.3.9e.tar.gz) and apply that patch with patch -p1. Compiled-in modules: mod_auth.c mod_auth_file.c mod_auth_unix.c mod_ban.c mod_cap.c mod_core.c mod_ctrls.c mod_delay.c mod_facts.c mod_log.c mod_ls.c mod_rlimit.c mod_site.c mod_tls.c mod_xfer.c. mod_copy, mod_sftp, mod_sql, mod_rewrite, mod_exec and every other optional module are NOT compiled in. The complete corresponding source is ON THIS IMAGE in /usr/local/share/doc/proftpd/source/: the verified upstream tarball /usr/local/share/doc/proftpd/source/proftpd-1.3.9e.tar.gz, its detached signature /usr/local/share/doc/proftpd/source/proftpd-1.3.9e.tar.gz.asc, its checksum file /usr/local/share/doc/proftpd/source/proftpd-1.3.9e.tar.gz.sha256, the signing public key /usr/local/share/doc/proftpd/source/proftpd-signing-key-697E684D1668D6968428405CB78E893FA511976A.asc and the sac patch /usr/local/share/doc/proftpd/source/proftpd-1.3.9e-mod_ban-shm-0600.patch, together with the configure line given above. To rebuild the binary: extract the tarball (tar -xzf proftpd-1.3.9e.tar.gz), change into the proftpd-1.3.9e directory, run patch -p1 < proftpd-1.3.9e-mod_ban-shm-0600.patch, run the configure line given above, then run make. Upstream also publishes the tarball at ftp://ftp.proftpd.org/distrib/source/proftpd-1.3.9e.tar.gz and the release page is https://github.com/proftpd/proftpd/releases/tag/v1.3.9e. Written offer under GPL-2.0 section 3(b): for three years from the date this image was published, sac will provide to anyone who asks the complete corresponding source code of the ProFTPD binary on this image, on request to sales@secureanycloud.com, for no more than the cost of physically performing the distribution. It is not the Ubuntu proftpd package, so apt and unattended-upgrades do not update it; sac publishes a new image version when upstream releases a fix (upstream: http://www.proftpd.org/ and https://github.com/proftpd/proftpd/releases).

The complete corresponding source is on the image in /usr/local/share/doc/proftpd/source/: the unmodified upstream tarball, its detached signature, its checksum file, the maintainer's signing key and the SecureAnyCloud patch:

ls -l /usr/local/share/doc/proftpd/source/
total 19424 -rw-r--r-- 1 root root 280 Oct 31 02:29 proftpd-1.3.9e-mod_ban-shm-0600.patch -rw-r--r-- 1 root root 19870575 Oct 31 02:29 proftpd-1.3.9e.tar.gz -rw-r--r-- 1 root root 195 Oct 31 02:29 proftpd-1.3.9e.tar.gz.asc -rw-r--r-- 1 root root 88 Oct 31 02:29 proftpd-1.3.9e.tar.gz.sha256 -rw-r--r-- 1 root root 1615 Oct 31 02:29 proftpd-signing-key-697E684D1668D6968428405CB78E893FA511976A.asc

You can verify this yourself. Check the checksum and the signature of the tarball and the unchanged upstream line, and confirm that the patch applies and produces the line in the binary on this image:

cd /usr/local/share/doc/proftpd/source sha256sum -c proftpd-1.3.9e.tar.gz.sha256
W=$(mktemp -d) gpg --dearmor < proftpd-signing-key-697E684D1668D6968428405CB78E893FA511976A.asc > "$W/proftpd.gpg" gpgv --keyring "$W/proftpd.gpg" proftpd-1.3.9e.tar.gz.asc proftpd-1.3.9e.tar.gz 2>&1 tar -xzf proftpd-1.3.9e.tar.gz -C "$W" proftpd-1.3.9e/contrib/mod_ban.c proftpd-1.3.9e/NEWS grep -A1 'Issue 2388' "$W/proftpd-1.3.9e/NEWS" echo "upstream: $(sed -n 835p "$W/proftpd-1.3.9e/contrib/mod_ban.c")" (cd "$W/proftpd-1.3.9e" && patch -p1 < /usr/local/share/doc/proftpd/source/proftpd-1.3.9e-mod_ban-shm-0600.patch && echo "patched: $(sed -n 835p contrib/mod_ban.c)") rm -rf "$W"
proftpd-1.3.9e.tar.gz: OK gpgv: Signature made Mon Oct 26 22:55:44 2026 UTC gpgv: using DSA key 697E684D1668D6968428405CB78E893FA511976A gpgv: Good signature from "TJ Saunders <tj@castaglia.org>" - Issue 2388 - mod_ban SysV shared memory segment default permissions allow for local unprivileged user read/write access. upstream: shmid = shmget(key, sizeof(struct ban_data), IPC_CREAT|IPC_EXCL|0666); patching file contrib/mod_ban.c patched: shmid = shmget(key, sizeof(struct ban_data), IPC_CREAT|IPC_EXCL|0600);

Rebuild it. The image carries no compiler. On any Ubuntu 24.04 machine, copy the five files from /usr/local/share/doc/proftpd/source/ and run:

sudo apt-get install -y build-essential libssl-dev libcap-dev libcrypt-dev tar -xzf proftpd-1.3.9e.tar.gz && cd proftpd-1.3.9e patch -p1 < ../proftpd-1.3.9e-mod_ban-shm-0600.patch ./configure --prefix=/usr/local --sysconfdir=/etc/proftpd --localstatedir=/run/proftpd \
  --with-modules=mod_tls:mod_ban --enable-openssl --enable-ctrls --enable-cap \
  --disable-auth-pam --disable-ident --disable-dso --disable-nls --disable-sodium \
  --disable-memcache --disable-redis --disable-pcre --disable-pcre2 --disable-facl \
  --disable-curses --disable-ncurses make

The upstream tarball is also published at ftp://ftp.proftpd.org/distrib/source/proftpd-1.3.9e.tar.gz, with its checksum and signature on the release page https://github.com/proftpd/proftpd/releases/tag/v1.3.9e.

Written offer. For three years from the date this image was published, SecureAnyCloud will provide the complete corresponding source code of the ProFTPD binary on this image to anyone who asks, on request to sales@secureanycloud.com, for no more than the cost of physically performing the distribution. The license text is /usr/local/share/doc/proftpd/COPYING. The file contrib/mod_tls.c carries a BSD license (/usr/local/share/doc/proftpd/LICENSE.mod_tls).

15

Updates

The operating system and OpenSSL update through apt, and unattended security upgrades are on. ProFTPD itself is the source build described above. It is not the Ubuntu proftpd-core package, so apt and unattended-upgrades never update it:

/usr/local/sbin/proftpd -v apt-cache policy proftpd-core | sed -n 1,3p
ProFTPD Version 1.3.9e proftpd-core: Installed: (none) Candidate: 1.3.8.b+dfsg-1ubuntu0.1

Do not install Ubuntu's proftpd-core or proftpd-basic package. It is the older 1.3.8b and would conflict with the server on port 21.

When a ProFTPD release fixes a security issue, SecureAnyCloud adds a new image version to this listing. Track upstream at http://www.proftpd.org/ and https://github.com/proftpd/proftpd/releases (this image: version 1.3.9e). To move to a new version:

  • Launch the new version next to the old instance.
  • Stop the server on the old instance (sudo systemctl stop proftpd), take a snapshot of its data volume, and create a volume from that snapshot in the new instance's Availability Zone.
  • On the new instance, stop the server and attach the volume. Then copy your users' directories into /srv/ftp, keeping ownership with sudo cp -a. Alternatively, mount the volume in place of the new one by its UUID in /etc/fstab.
  • Copy /etc/proftpd/ftpd.passwd and /etc/proftpd/ftpd.group from the old instance (mode 0600, owner root), then start the server. Your users keep their passwords. The new instance's own SecureAnyCloud password is in its credentials file.
16

Server components

Component Version Path

ProFTPD 1.3.9e (signed upstream source build, one disclosed patch) /usr/local/sbin/proftpd

ftpdctl, ftpwho, ftpcount 1.3.9e /usr/local/bin/

OpenSSL 3.0 (Ubuntu, updated by apt) system library

Ubuntu 24.04 LTS

Path Purpose

/etc/proftpd/proftpd.conf Server configuration

/etc/proftpd/ftpd.passwd, ftpd.group Virtual users and the sacftp group (mode 0600)

/etc/proftpd/sac-masquerade.conf Passive address, written at every boot from /etc/sac/proftpd-masquerade or instance metadata

/etc/proftpd/tls/ Certificate and key (directory mode 0700)

/var/lib/sac/data/proftpd-tls-sac.sha256 Fingerprint of the certificate SecureAnyCloud created (Step 12)

/srv/ftp/<user> Each user's jail on the data volume

/var/log/proftpd/ Transfer log (xferlog), TLS, ban and control logs; sessions and logins go to the journal (journalctl -u proftpd)

/root/proftpd-credentials.txt Host, user, password and certificate fingerprint (mode 0600)

/usr/local/share/doc/proftpd/ Licence, NEWS, release notes, SOURCE.txt, the patch and the complete source

/usr/local/sbin/proftpd-* Account, host name, passive address, check, health and leak check tools

To change a setting, edit /etc/proftpd/proftpd.conf, check it with sudo /usr/local/sbin/proftpd -t, and then run sudo systemctl restart proftpd. Always restart after a check. The check re-creates the control socket, so ftpdctl cannot reach the running server until it restarts.

Troubleshooting

  • The client connects but the listing hangs or times out. The passive data connection is blocked: allow TCP 40000-40099 in the security group (and any network ACL or corporate firewall) from the client's address. Behind NAT or an Elastic IP, set the passive address (Step 11).
  • "550 SSL/TLS required on the control channel". The client tried plain FTP. Choose explicit FTP over TLS (Step 5).
  • "534 Unwilling to accept security parameters" after PROT C. The client encrypts the login but not the data. In lftp, set ftp:ssl-protect-data true. In other clients, choose Require explicit FTP over TLS.
  • "425 Unable to build data connection: Operation not permitted". The client did not reuse the TLS session on the data connection (/var/log/proftpd/tls.log says client did not reuse TLS session). Use a current FileZilla, WinSCP or lftp. With the curl built into macOS, add --tls-max 1.2 (Step 5).
  • "501 PORT: Operation not permitted" or "501 EPRT: Operation not permitted". The client is in active mode. Switch it to passive mode.
  • "530 Login incorrect". The user name or password is wrong. Three failures end the session.
  • "530 Host ... has been banned" or the connection closes at once. The address is banned after repeated failed logins (Step 9). Lift the ban with sudo ftpdctl permit host <client-ip> and fix the stored password on that client.
  • The certificate warning names the wrong host after a stop and start. The public address changed. Run sudo proftpd-set-hostname, or use an Elastic IP or DNS name (Step 12).
  • Nothing answers after launch. Check systemctl status proftpd-firstboot and journalctl -u proftpd-firstboot. Until first boot completes, the server is deliberately closed.

Security recommendations

  • Allow ports 21 and 40000-40099 only from the addresses your FTP users connect from, and port 22 only from your administrators.
  • Give each person or system its own user, and delete users you no longer need.
  • Compare the certificate fingerprint the first time a client connects, or install a certificate your clients trust (Step 12).
  • Keep the operating system patched (unattended security upgrades are on), and move to new image versions when SecureAnyCloud publishes a ProFTPD update (Step 15).
  • Back up the data volume and the user files (Step 13).

Support and licensing

ProFTPD is open source under the GNU General Public License, version 2 or later, with the authors' exemption for linking with OpenSSL. The mod_tls module carries a BSD license. The source, the one SecureAnyCloud change and the written offer are described in Step 14. There is no per-user or per-CPU license fee. The AMI price covers SecureAnyCloud's build, hardening, first-boot setup and support. ProFTPD is a trademark of its respective owner; SecureAnyCloud is not affiliated with, endorsed by or sponsored by the ProFTPD Project.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24-hour response SLA

Deploy on AWS

Launch ProFTPD 1.3.9e on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS