Building a Secure FTP Server
Building a secure FTP server should begin with a clear understanding of the mechanisms involved. When we talk about an FTP server, this commonly involves three protocols:
- FTP — File Transfer Protocol. This is your basic protocol for transferring files.
- FTPS — FTP over SSL, or FTP Secure. This is an extension to the basic FTP protocol, which adds support for TLS (Transport Layer Security).
- SFTP — While the acronym is similar, this is SSH File Transfer Protocol, and it operates unrelated to both FTP and FTPS.
Deciding which protocols work best for you, depends largely on your project. There should almost never be a need to support basic FTP. The exception might be if you needed a simple file transfer solution between servers that exist within a private local network. Otherwise, using FTP is akin to serving an application over HTTP instead of HTTPS.
Let’s explore some of the key differences between each protocol.
FTP | FTPS | SFTP | |
---|---|---|---|
Command Port | 21 | 990 | 22 |
Data Port | 20 | 989 (active mode) — passive mode is user defined, but by default any port 0—65535 | 22 |
Security | Basic FTP doesn’t encrypt any communication between the client and the server | Command and data channels are encrypted only if the client issues the necessary AUTH and PROT commands | Relies on SSH for data encryption over the wire - commands and data are always encrypted |
Connections | At least 2: one port to issue commands and a separate port for data | Only 1 is required (commands and data use the same connection) | |
Pros | Anonymous FTP access in browser, and slightly faster due to having no encryption overhead | Widely known and supported, with better support for server-to-server file transfers | Commands and data are always encrypted, and is backed by solid standards |
Cons | Connection details and data is transmitted in clear text | Requires a secondary DATA channel, which makes it harder to use behind firewalls and NATs | Limited server-to-server options, dependent on environment and application |
Implicit -vs- Explicit in FileZilla
It’s worth noting that explicit FTPS uses port 21, where implicit FTPS uses port 990. With explicit mode, clients initially connect to the standard FTP port (21), and then upgrades the connection into secure FTPS mode (990), by issuing an AUTH command. By comparison, with implicit mode, its assumed the connection is always encrypted from the beginning.
As described in this FileZilla Wiki, Explicit mode is considered more modern. When also considering that most clients and software libraries assume port 21 as the default, Explicit is recommended over Implicit.
You can switch to implicit mode by listening on port 990 instead of 21, and enabling the implicit_ssl option in vsftpd.conf
:
Getting Started
I’m going to show you how to build a secure FTP server with the following features:
- Support for both FTPS and SFTP to maximize integration options for third-parties.
- Virtual FTPS users with custom authentication using a Berkeley DB database.
- Jailed user environments, so that users cannot access any files or directories outside of their own dedicated folders.
- Shell-less SFTP users, so that users can only perform file transfer operations.
- A command script for easy user management.
You’re going to need the following tools and software:
- A linux-based virtual machine — I’ve used Ubuntu 20.04.
- vsftpd (Very Secure FTP Daemon) — this is our FTP server software.
- db-util — Berkeley DB database utilities (this will be for our user database)
- SSL certificate (if you plan on creating your own DNS record) — otherwise, we can generate a self-signed certificate.
Step 1: Install vsftpd
and db-util
Step 2: Configure vsftpd
Open the configuration file:
and replace the entire contents with the configuration below. I’ve added comments describing what each of the options are for. If you’re looking for additional information, I recommend the Red Hat Documentation on vsftpd, as well as Ubuntu’s community help wiki.
Be sure to look at the pasv_address
, rsa_cert_file
and rsa_private_key_file
options at the bottom. You will need to update these values related to your own server.
Step 3: Configuring firewalls
In passive mode, the client initiates a PASV command to the server, which requests an available port for data transmission. By default, vsftpd does not limit the port range, meaning a client could be returned a port anywhere between 0—65535.
Instead, we’ve defined a data port range between 50000—50999 in our vsftpd.conf
, which gives us 999 available data ports for clients. If you anticipate having significant numbers of concurrent users, consider increasing this range.
Now that we’ve defined an explicit range, we need to allow this range of ports in our firewall. If you’re using Microsoft Azure, apply these rules to a Network Security Group that covers your FTP server. If you’re using Ubuntu’s Uncomplicated Firewall (UFW), first make sure its enabled:
enable it if you need to:
then allow the TCP port range we defined in our vsftp.conf
config:
In addition, we must also allow default ports common to the FTPS and SFTP protocols. Again, if you’re using Azure, apply these rules to a Network Security Group. Otherwise, update your Ubuntu firewall:
Here is a recap of the ports we’re allowing:
-
20
— FTP data channel -
21
— FTP command channel -
22
— SSH -
989
— FTPS data channel (active mode) -
990
— FTPS command channel -
50000—50999
— FTPS data channels (passive mode)
Step 4: Create a new PAM service
PAM (short for Pluggable Authentication Modules), is a powerful suite of libraries that allow us to dynamically authenticate users in a Linux-based system. We’re going to use the pam_userdb module which will allow us to authenticate against a DB database.
Create a new PAM file that will use our new database (you’ll create the database in step 6):
and save the following:
Note that the path to the database file should be specified without the .db
suffix.
As an extra, pam_userdb
allows us to define whether passwords stored in our user database, are encrypted, by passing an additional crypt option.
It’s important to know, if you choose this option, passwords must be stored in crypt(3) form. The crypt()
function relies on the legacy DES (Data Encryption Standard) algorithm, which only supports a maximum password length of 8 characters. This is not often expressly pointed out, but is described in the related manual:
By taking the lowest 7 bits of each of the first eight characters of the key, a 56-bit key is obtained. This 56-bit key is used to encrypt repeatedly a constant string (usually a string consisting of all zeros).
Step 5: Create service directories
We’re going to need some directories where user content will live. It is important that user directories are created inside a parent directory, which will act as our jail. We’ll also need a place to keep our virtual user database.
Step 6: Create FTPS user database
Rather than creating local users for FTPS, we’re going to create virtual users — a feature of vsftpd. Enabling virtual FTPS users will help enhance the security of our FTP server.
First create a plain text file:
then enter your usernames and passwords on alternating lines, as described in the db_load documentation:
If the database to be created is of type Btree or Hash, or the keyword keys is specified as set, the input must be paired lines of text, where the first line of the pair is the key item, and the second line of the pair is its corresponding data item.
For example, we’re going to create the users batman
with password bat!
, and robin
with password cave!
:
Next, we’ll use db_load to generate our user database. This will take users.txt
as input, and output users.db
:
The arguments we’re passing here are:
-
-T
— allows non-Berkeley DB applications to easily load text files into databases. -
-t <method>
— specify the underlying access method (required when using-T
). Here we’re using the Hash access method, which is best suited for large data sets (ie: many users), where we aren’t concerned about sequential access. The hash method is also more memory efficient, as we can typically access data with a single I/O operation (compared to B-tree for example). -
-f <file>
— read from the specified input file. -
<output>
— the last argument is our desired output DB file.
Once you’ve created your user database, update its permissions:
You should also delete users.txt
, if you no longer need it:
Step 7: Create user directories
Now we need directories for our new users:
Step 8: Create SFTP users
Since SFTP users are local users, let’s go ahead and create them — keeping in mind that these are not the same as our virtual FTPS users.
Here we’re passing the --shell <shell>
argument, which defines what shell is loaded for the user on login. In this case, we’re supplying /bin/false
, which is actually no shell at all. This effectively removes the user’s shell access, ensuring they can only use their access for SFTP file transfers.
Step 9: Jailing FTPS users
In order to jail our users, which we’ll accomplish using chroot, we need to set some very specific permissions, and make a few changes to our configuration files.
First, make sure the owner of our FTPS parent directory, and all user subdirectories, matches our guest_username
. The username is defined in our vsftpd.conf
config, and in this case, it’s ftp
:
Next, remove all group permissions on our parent FTPS directory:
Step 10: Jailing SFTP users
In order to jail our SFTP users, we’ll need to create a group, under which all SFTP users must belong — we’ll call it sftponly
:
Now let’s add our users to this group:
Next, we need to make some configuration changes to our SSH service. Open the sshd_config file:
find this line:
and replace with:
What we’re doing here, is defining the external subsystem (eg: file transfer daemon), which is started automatically after SSH login from the client. The internal-sftp
value implements an in-process SFTP server that requires no support files when defining a ChrootDirectory
. Basically, it simplifies the process allowing us to force a different filesystem root on our users (jail them).
Now lets create a conditional block, using Match, that will apply some options to any user belonging to the sftponly
group:
There’s a few important things to understand about these options.
-
ForceCommand forces the execution of the command specified, ignoring any commands supplied by the client. By default, when an SSH user logs in, they would land in our
/home/sftp
directory, thereby allowing them to see all other users that might exist. While they won’t be able to access those directories, it’s better they don’t see them at all. To fix this, we’re going to force a directory change upon login, by passing the-d <path>
argument, where/%u
is our path relative to our ChrootDirectory, and%u
is a token that represents the username. So on login, the user should automatically be brought to/home/sftp/batman
without knowing it. -
PasswordAuthentication you can set this to either
no
oryes
depending if you want to allow SFTP users to authenticate with passwords. The alternative being private/public SSH keys, which are safer, though requires more involvement to manage. I recommend allowing passwords, so long as you are creating users with cryptographically strong passwords. -
ChrootDirectory specifies the pathname of the directory to chroot to after the user authenticates. A requirement of Chroot, is that
root
be the owner of the jailed directory.
Lastly, lets set some required permissions — note that 0711
grants public execute, but limits read and write to the owner (root), as required by Chroot:
User command script
In order to easily manage your FTP users, we’re going to create a command script to do the work for us. This script will enable you to:
- List all existing users —
./path/to/users list
- Add a new user —
./path/to/users add <username> <password>
- Edit an existing user —
./path/to/users edit <username> <password>
- Delete a user —
./path/to/users del <username>
This script is written in Perl, and is largely just a series of linux commands. I’ve included links to some helpful documentation:
Create a new file — you can include the .pl
extension if you prefer:
To help facilitate future updates, i’ll include the script as a gist:
- 🤖 users.pl — Perl script for managing users in a Berkeley DB within a Linux environment.
Remote control over API
As a final consideration, you could further integrate your FTP control system, by building an API. This API would SSH into your FTP server, and execute commands using the command script. You can then build a graphical interface to manage your FTP users.
Here’s a quick example written in PHP, which makes use of phpseclib:
and in Python — using Paramiko: