Golang FTP Server
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.
Supported backends
| 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).
Features
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
- 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)
Getting started
Download it
Fetch a binary for your platform from the latest release
and run it. Archives are named ftpserver- and are provided for Linux, macOS and Windows on
amd64 and arm64, plus armv6/armv7 for Linux (Raspberry Pi and friends).
Build it
go install github.com/fclairamb/ftpserver@latest
ftpserver
Run it with docker
Images 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,: minimal image, containing only the binaryalpine,: image based on alpine, if you need a shell-alpine
/app directory, which is where it reads (or creates) its ftpserver.json config file. The
default config file serves the /tmp directory.
# 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/ftpserver
Run it with docker compose
# docker-compose.yml
services:
ftpserver:
image: fclairamb/ftpserver
ports:
- '2121-2130:2121-2130'
volumes:
- ./files:/tmp
- .:/app
docker compose up -d
Test it
# Create some file
head -c 1048576 /dev/urandom > file.bin
Upload it to the server
curl -v -T file.bin ftp://test:test@localhost:2121/
Download it back
curl ftp://test:test@localhost:2121/file.bin -o file2.bin
Compare it
cmp file.bin file2.bin
Usage
ftpserver [-conf <file>] [-conf-only]
-conf: 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.
SIGHUPreloads the config file.SIGTERMstops accepting new connections and waits up to one minute for the connected clients to leave.
Configuration
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/test user serving /tmp.
{
"$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.
General options
| 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 | |
Accesses
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.
Local disk
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"
}
}
Webhook authentication
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.
TLS
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.pem
Contributing
See CONTRIBUTING.md.