ProFTPD 1.3.9e on AWS User Guide
Overview
This guide covers launching and using the ProFTPD 1.3.9e on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg File Sharing and Sync listing on AWS Marketplace; you pick 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: scanners and cameras that upload over FTP, line of business systems that drop files every night, or people who use FileZilla, WinSCP or Cyberduck. The cloudimg image makes it safe to put on a network:
- FTPS only. Explicit FTP over TLS (
AUTH TLSon 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, and 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
cloudimgwith a random 24 character password and a TLS certificate for your instance, and 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
cloudimgftpgroup; the Linux accounts on the instance (includingrootandubuntu) 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 unprivilegedftpvirtualaccount. No ProFTPD process runs as root: the listening process runs as theproftpdaccount with only the Linux capabilities it needs. - Automatic bans and slow guessing. A client address that keeps failing to log in is banned for an hour (mod_ban), connection floods are banned for 15 minutes, and 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(theSITE CPFR/SITE CPTOcommands behind several past ProFTPD vulnerabilities) andmod_sftpare 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, starts the server, proves a full encrypted round trip and that no password leaked into a log, and only then finishes; if anything fails, the server is stopped 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 Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What's on the image
- ProFTPD 1.3.9e, built by cloudimg from the signed upstream source, 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 tarball's SHA-256 checksum and its detached signature from the ProFTPD maintainer's key were both verified. It is built against Ubuntu's OpenSSL 3, the compiler was removed again afterwards, and the complete corresponding source ships on the image (Step 14). - One cloudimg 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 upgradeand 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, cloudimg 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 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, from your own networks and your FTP users' addresses only:
- TCP 22 for SSH administration,
- TCP 21 for the FTPS control connection,
- TCP 40000-40099 for passive data connections (the exact range the server uses).
- An FTPS client: FileZilla, WinSCP, Cyberduck,
lftporcurl.
Do not open ports 21 and 40000-40099 to 0.0.0.0/0 unless the server really 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 them out, but every attempt still costs you log noise and connections. List the address ranges your users and systems connect from instead.
Step 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 and your Region, then Launch through EC2. Choose m5.large (FTP needs little CPU; size for your users and transfer volume), your key pair, and a security group as 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.
With the AWS CLI (replace the values in angle brackets; the second 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 (here the documentation range 203.0.113.0/24; use your own):
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}]'
Step 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, 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.
Step 3: Check the services
First boot creates the user and the certificate, starts the server and proves an encrypted round trip before it finishes (usually under a minute):
systemctl is-active proftpd proftpd-prepare proftpd-firstboot
sudo test -f /var/lib/cloudimg/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:
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
Step 4: Get your user and password
The credentials file is readable by root only. This shows 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=cloudimg
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/cloudimg
TLS_CERT_SHA256=<certificate-sha256-fingerprint>
600 root /root/proftpd-credentials.txt
To print the password itself (do this where nobody can read your screen, and do not paste it 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 (the public or private IP address when the subnet has no public DNS names). 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.
Step 5: Connect with an FTPS client
In FileZilla, open File > Site Manager > New site and set:
| 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 | cloudimg and the password from Step 4 |
On the Transfer Settings tab keep Passive mode (active mode is refused by the server). The first time you connect, FileZilla shows the server's certificate: it is self signed and was created on your instance, so compare its SHA-256 fingerprint with TLS_CERT_SHA256 from Step 4 before you tick 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 = cloudimg
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. --ssl-reqd 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 cloudimg -T ./report.txt ftp://<FTP_HOST>/report.txt
curl --ssl-reqd --cacert proftpd.crt -u cloudimg --list-only ftp://<FTP_HOST>/
curl --ssl-reqd --cacert proftpd.crt -u cloudimg ftp://<FTP_HOST>/report.txt
curl --ssl-reqd --cacert proftpd.crt -u cloudimg -Q 'DELE report.txt' -o /dev/null ftp://<FTP_HOST>/
Run from a laptop outside AWS against a test instance, this 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 cloudimg <FTP_HOST>
lftp cloudimg@<FTP_HOST>:~> set ftp:ssl-force true
lftp cloudimg@<FTP_HOST>:~> set ftp:ssl-protect-data true
lftp cloudimg@<FTP_HOST>:~> set ssl:ca-file proftpd.crt
lftp cloudimg@<FTP_HOST>:~> put lftp.txt
lftp cloudimg@<FTP_HOST>:~> ls
-rw-r--r-- 1 <user> cloudimgftp 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 (Homebrew's, for example).
Step 6: Upload and download from the instance
This uploads a file, reads it back, lists the directory and deletes the file, all over FTPS to the instance's own private address, checking 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 cloudimg" > /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 cloudimg
list -> report.txt
delete -> ok
Uploaded files land in the user's jail, /srv/ftp/cloudimg, owned by the unprivileged ftpvirtual account. To drop files in for a user over SSH, copy them there and give them to that account (sudo chown ftpvirtual:cloudimgftp /srv/ftp/cloudimg/<file>).
Step 7: Run the built in check
The image ships an end to end check you can run at any time; first boot runs it too. It 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) cloudimg-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/cloudimg
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>
It 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.
Step 8: Security checks
A client that does not start TLS gets the welcome banner, which names no software or version, and is then refused 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:
/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, allows only TLS 1.2 and 1.3, uses only ProFTPD's own user file, lets only the cloudimgftp group log in, jails everyone, refuses active mode and the SITE copy commands, and 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/cloudimg-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 cloudimgftp
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:
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/cloudimg
sudo cut -d: -f1,6,7 /etc/proftpd/ftpd.passwd; sudo cut -d: -f2 /etc/proftpd/ftpd.passwd | cut -c1-14
getent passwd cloudimg || echo "cloudimg: 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:cloudimgftp /srv/ftp/cloudimg
cloudimg:/srv/ftp/cloudimg:/usr/sbin/nologin
$6$rounds=1000
cloudimg: 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-cloudimg-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
# cloudimg: 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
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 really connected from (never a forwarded header), so the journal and the transfer log 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 cloudimg: 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/cloudimg/cloudimg-verify-live-18711.bin b _ i r cloudimg ftps 0 * c
Sat Oct 31 02:50:10 2026 0 <private-ip> 131072 /srv/ftp/cloudimg/cloudimg-verify-live-18711.bin a _ d r cloudimg ftps 0 * c
Step 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
This is 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>: cloudimg FTPS server (0.0.0.0#21)
If you ban one of your own systems by mistake (a scanner with an old password, for example), lift the ban for its address, 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, and a restart of the server clears them.
Step 10: Users and passwords
Every user gets its own jail, /srv/ftp/<user>, and never sees the others. Add a user with a generated password (shown once):
sudo proftpd-adduser alice
Or set the password yourself on standard input, so it never appears on a command line or in your history (at least 12 characters):
read -rs PW && printf '%s\n' "$PW" | sudo proftpd-adduser bob --password-stdin; unset PW
Change a password (generated, or --password-stdin); for the cloudimg 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 cloudimg
Delete a user; add --remove-files to delete its directory too (it removes only a real ftpvirtual owned directory directly under /srv/ftp, never a link):
sudo proftpd-deluser alice --remove-files
List the users and their jails:
sudo proftpd-listusers
cloudimg /srv/ftp/cloudimg
Step 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 the server announces from 40000-40099, so your security group must allow that range as well as port 21, from the same client addresses. Active mode, where the server would connect back to the client (PORT/EPRT), is refused, so the server 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 only need the port. Older clients use PASV, where the server also announces an address (MasqueradeAddress). A fresh instance only knows its private address, so on every boot proftpd-prepare (a oneshot service that runs before ProFTPD) chooses the address:
- If
/etc/cloudimg/proftpd-masqueradeexists, its first line is used: an IPv4 address, a DNS name, ornone. - Otherwise the instance's public IPv4 address from instance metadata (IMDSv2) is used. It can change when you stop and start the instance, which is why this runs at every boot.
- With neither (a private-only instance), no
MasqueradeAddressis written andPASVannounces the address the client connected to.
cat /etc/proftpd/cloudimg-masquerade.conf
# Written by proftpd-prepare at every boot - do not edit. Set your own passive address in /etc/cloudimg/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 cloudimg --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/cloudimg/proftpd-masquerade; write none when every client is inside the VPC or connects over a VPN or Direct Connect. Then apply it (this rewrites the generated file and restarts ProFTPD, which ends open sessions):
echo 198.51.100.25 | sudo tee /etc/cloudimg/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/cloudimg-masquerade.conf itself; it is rewritten at every boot.
If you change PassivePorts in /etc/proftpd/proftpd.conf, change the security group to the same range.
Step 12: Your own DNS name and certificate
The rule: cloudimg only ever replaces its own certificate. When first boot creates the instance's self signed certificate, it records that certificate's SHA-256 fingerprint in the root owned file /var/lib/cloudimg/proftpd-tls-cloudimg.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 one is installed:
CUR=$(sudo openssl x509 -noout -fingerprint -sha256 -in /etc/proftpd/tls/proftpd.crt | cut -d= -f2)
if [ "$CUR" = "$(cat /var/lib/cloudimg/proftpd-tls-cloudimg.sha256)" ]; then echo "cloudimg self signed certificate $CUR"; else echo "your own certificate $CUR"; fi
cloudimg 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 cloudimg's certificate is installed, this creates a new self signed certificate for the name and the instance's current addresses, records its fingerprint, updates FTP_HOST and TLS_CERT_SHA256 in /root/proftpd-credentials.txt, restarts the server and keeps the name for later runs. Run it 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. With the domain in Route 53 and an instance role 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. Any certificate from your own certificate authority is installed the same way.
Back to a self signed certificate. proftpd-set-hostname refuses to touch a certificate cloudimg did not create (the installed certificate was not minted by cloudimg (yours?) - not replacing it). To replace yours with a new cloudimg self signed certificate, say so explicitly; remove the TLSCertificateChainFile line first if you added one:
sudo proftpd-set-hostname --replace-own-cert
Step 13: The data volume
/srv/ftp is its own EBS volume, 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 it, increase the volume's size in the EC2 console (Volumes > Modify volume), wait until the modification reaches the optimizing state, then grow the file system online (the next boot does it too):
sudo proftpd-prepare
Back it up with EBS snapshots or AWS Backup; a snapshot of the data volume holds every user's files. Back up /etc/proftpd/ftpd.passwd and /etc/proftpd/ftpd.group as well, which hold your users.
Step 14: Source code, the cloudimg change and the licence
ProFTPD is licensed under the GNU General Public License, version 2 or later. This image's ProFTPD is built from the signed upstream 1.3.9e tarball with one cloudimg 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.
Why: 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 cloudimg applies the one line change itself. The result is visible on a running instance 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 cloudimg 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 cloudimg 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 cloudimg 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, cloudimg will provide to anyone who asks the complete corresponding source code of the ProFTPD binary on this image, on request to support@cloudimg.co.uk, 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; cloudimg 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 cloudimg 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
Verify it yourself: the checksum and the signature of the tarball, the unchanged upstream line, and 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, cloudimg will provide the complete corresponding source code of the ProFTPD binary on this image to anyone who asks, on request to support@cloudimg.co.uk, for no more than the cost of physically performing the distribution. The licence text is /usr/local/share/doc/proftpd/COPYING; contrib/mod_tls.c carries a BSD licence (/usr/local/share/doc/proftpd/LICENSE.mod_tls).
Step 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, cloudimg 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, attach the volume and copy your users' directories into
/srv/ftp(keeping ownership:sudo cp -a), or mount the volume in place of the new one by its UUID in/etc/fstab. - Copy
/etc/proftpd/ftpd.passwdand/etc/proftpd/ftpd.groupfrom the old instance (mode0600, ownerroot), then start the server. Your users keep their passwords; the new instance's owncloudimgpassword is in its credentials file.
Step 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 cloudimgftp group (mode 0600) |
/etc/proftpd/cloudimg-masquerade.conf |
Passive address, written at every boot from /etc/cloudimg/proftpd-masquerade or instance metadata |
/etc/proftpd/tls/ |
Certificate and key (directory mode 0700) |
/var/lib/cloudimg/proftpd-tls-cloudimg.sha256 |
Fingerprint of the certificate cloudimg 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, 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 setftp: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.logsaysclient did not reuse TLS session). Use a current FileZilla, WinSCP or lftp; with macOS's built in curl 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". Wrong user or password. 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 it 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-firstbootandjournalctl -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; 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 cloudimg 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; mod_tls carries a BSD licence. The source, the one cloudimg change and the written offer are described in Step 14. There is no per user or per CPU licence fee; the AMI price covers cloudimg's build, hardening, first boot setup and support. ProFTPD is a trademark of its respective owner; cloudimg is not affiliated with, endorsed by or sponsored by the ProFTPD Project.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA