A gateway between old-school FTP devices (cameras, scanners, printers, legacy software...) and modern cloud based file systems.
It is built on top of ftpserverlib, for which it is the reference
implementation, and of the afero Fs interface.
| Backend | fs value |
Documentation |
|---|---|---|
| Local disk | os |
below |
| Amazon S3 and S3-compatible storages (MinIO, Ceph...), through afero-s3 | s3 |
fs/s3 |
| Google Cloud Storage, through afero's gcsfs | gcs |
fs/gcs |
| Dropbox, through afero-dropbox | dropbox |
fs/dropbox |
| Google Drive, through afero-gdrive | gdrive |
fs/gdrive |
| SFTP, through afero's sftpfs | sftp |
fs/sftp |
| Email (each uploaded file is sent as an attachment), thanks to @x-way | mail |
fs/mail |
| Telegram (each uploaded file is sent to a chat), thanks to @slayer | telegram |
fs/telegram |
| Local disk with users authenticated by Keycloak | keycloak |
fs/keycloak |
On top of any backend, an access can be made read-only, share its backend instance between sessions, write through a local temporary directory or log every file access (see fs/README.md).
FTP protocol support is brought by ftpserverlib:
- Uploading and downloading files, with resume support (
REST) - Directory listing (
LIST,NLST,MLST/MLSD) - File and directory deletion and renaming, symbolic links (
SITE SYMLINK, on the local disk backend) - Passive (
PASV,EPSV) and active (PORT,EPRT) transfers, IPv6 support (RFC 2428) - Explicit TLS (
AUTH TLS,PROT) and implicit TLS - File modification time (
MDTM) and size (SIZE) - File hashing (
HASH,XCRC,MD5,XSHA256...), when enabled - Small memory footprint
On top of it, this server provides:
- Multiple users, each one with their own backend
- Hashed passwords (bcrypt, md5crypt, sha256crypt, sha512crypt), plain-text passwords can be hashed automatically
- Authentication through an HTTP webhook
- Configuration hot-reload (
SIGHUP) and graceful shutdown (SIGTERM)
Fetch a binary for your platform from the latest release
and run it. Archives are named ftpserver-<version>-<os>-<arch> and are provided for Linux, macOS and Windows on
amd64 and arm64, plus armv6/armv7 for Linux (Raspberry Pi and friends).
go install github.com/fclairamb/ftpserver@latest
ftpserverImages are available on Docker Hub and on the
GitHub container registry
(ghcr.io/fclairamb/ftpserver) for linux/amd64, linux/arm64, linux/arm/v6 and linux/arm/v7:
latest,<version>: minimal image, containing only the binaryalpine,<version>-alpine: image based on alpine, if you need a shell
The image runs from the /app directory, which is where it reads (or creates) its ftpserver.json config file. The
default config file serves the /tmp directory, with a test user whose random password is printed in the logs
(docker logs <container>) when the file is created.
# Creating a directory
mkdir -p files
# Starting the sample FTP server
docker run --rm -d -p 2121-2130:2121-2130 -v $(pwd)/files:/tmp -v $(pwd):/app fclairamb/ftpserverThe images run as the unprivileged user 65532:65532. Mounted directories must be writable by that uid (for example
chown 65532:65532 files .), or you can override the user with --user "$(id -u):$(id -g)". This is a breaking change
for setups that mount host directories owned by another uid.
# docker-compose.yml
services:
ftpserver:
image: fclairamb/ftpserver
ports:
- '2121-2130:2121-2130'
volumes:
- ./files:/tmp
- .:/appdocker compose up -dThe password of the test user is generated when the config file is created, and only printed then:
# Find the generated password in the logs
docker logs <container> 2>&1 | grep "No conf file"
PASS=<the password from the logs>
# Create some file
head -c 1048576 /dev/urandom > file.bin
# Upload it to the server
curl -v -T file.bin "ftp://test:${PASS}@localhost:2121/"
# Download it back
curl "ftp://test:${PASS}@localhost:2121/file.bin" -o file2.bin
# Compare it
cmp file.bin file2.binftpserver [-conf <file>] [-conf-only]
-conf <file>: config file to use. When not specified,ftpserver.jsonis used, and created if it doesn't exist.-conf-only: only create the config file (if it doesn't exist) and check it can be loaded, then exit.
Signals:
SIGHUPreloads the config file.SIGTERMstops accepting new connections and waits up to one minute for the connected clients to leave.
The configuration is a JSON file, described by a JSON schema. Referencing it with the
$schema property gives you auto-completion and validation in most editors. If you start the server without any
config file, a sample one is created with a single test user serving /tmp. Its password is random: it is
printed once in the logs when the file is created, and stored hashed (bcrypt) in the file.
{
"$schema": "https://raw.githubusercontent.com/fclairamb/ftpserver/main/config-schema.json",
"version": 1,
"listen_address": "0.0.0.0:2121",
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
},
"accesses": [
{
"user": "test",
"pass": "test",
"fs": "os",
"params": {
"basePath": "/tmp"
}
},
{
"user": "s3",
"pass": "$2a$10$jG7tuqIlcUDMl1m1Ytj1TunU7pk.ko8lj3nOGzZvkIeU/BsfPVBra",
"fs": "s3",
"read_only": true,
"params": {
"bucket": "my-bucket",
"region": "eu-west-1"
}
}
]
}More samples are available in config/samples.
| Property | Description | Default |
|---|---|---|
listen_address |
Address to listen on | 0.0.0.0:2121 |
public_host |
Public IP address to advertise for passive transfers, when the server is behind a NAT gateway. Can be overridden with the PUBLIC_HOST environment variable |
|
passive_transfer_port_range |
Ports to use for passive transfers (start, end) |
any port |
max_clients |
Maximum number of simultaneous clients, 0 means no limit |
0 |
idle_timeout |
Duration after which idle connections are closed (e.g. 5m), a negative value disables it |
15m |
tls |
TLS certificate files: {"server_cert": {"cert": "cert.pem", "key": "key.pem"}} |
|
tls_required |
ClearOrEncrypted, MandatoryEncryption (explicit TLS required) or ImplicitEncryption (implicit FTPS) |
ClearOrEncrypted |
hash_plaintext_passwords |
Replace the plain-text passwords of the config file by their bcrypt hash | false |
extensions.enable_hash |
Enable the HASH command and its variants |
false |
logging.ftp_exchanges |
Log all the FTP commands and responses | false |
logging.file_accesses |
Log all the file accesses | false |
logging.file |
Also write the logs to this file | |
accesses |
The users and their backend, see below | |
accesses_webhook |
Authenticate users with a webhook instead of the accesses, see below |
Each access defines a user and the backend serving their files:
| Property | Description |
|---|---|
user |
User name |
pass |
Password: plain-text or hashed ($2a$/$2b$/$2y$ bcrypt, $1$ md5crypt, $5$ sha256crypt, $6$ sha512crypt). The anonymous user with a * password accepts any password |
fs |
Backend to use (see supported backends) |
params |
Backend parameters, as strings. See each backend's documentation |
read_only, shared, sync_and_delete, logging |
Common options, see fs/README.md |
Accesses are evaluated in their declaration order. A hashed password can be generated with
htpasswd -nbBC 10 "" 'my-password' | cut -d: -f2, or by enabling hash_plaintext_passwords.
The os backend serves a local directory, defined by the basePath parameter. Environment variables ($NAME) in
the path are replaced by their value.
{
"user": "test",
"pass": "test",
"fs": "os",
"params": {
"basePath": "/srv/ftp/$FTP_DIR"
}
}When accesses_webhook is defined, the accesses of the config file are ignored: for each login, the user and
password are posted as JSON ({"user": "...", "pass": "..."}) to the webhook. It must reply with a 200 status code
and an access object, any other status code refuses the login.
{
"accesses_webhook": {
"url": "https://auth.example.com/ftp",
"headers": {
"Authorization": "Bearer my-secret-token"
},
"timeout": "5s"
},
"accesses": []
}The timeout defaults to 30s.
You can generate a self-signed TLS key pair with the following command:
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out cert.pem -keyout key.pemSee CONTRIBUTING.md.