In data collection systems, IoT device logging, or secure document submission portals, we often need an FTP server that accepts uploads while preventing clients from viewing stored files. This write-only configuration enhances security by implementing the principle of least privilege.
Here are three proven methods to achieve write-only FTP functionality:
1. vsftpd Configuration
The most robust solution uses vsftpd with these settings in /etc/vsftpd.conf
:
# Basic security anonymous_enable=NO local_enable=YES write_enable=YES chroot_local_user=YES # Write-only configuration dirlist_enable=NO download_enable=NO force_dot_files=NO hide_ids=YES
2. Pure-FTPd Method
For Pure-FTPd servers, create a dedicated user with restricted permissions:
pure-pw useradd uploader -u ftpuser -d /var/ftp/uploads -m \ -N "::upload" -n 1000:10 -r 100:10 -t 1024 -T 50 -q 0
3. ProFTPD VirtualHost
ProFTPD supports virtual hosts with specific permissions:
<VirtualHost 192.168.1.100> ServerName "writeonly-ftp" DefaultRoot ~ <Limit WRITE> AllowAll </Limit> <Limit READ LIST> DenyAll </Limit> </VirtualHost>
For processing uploaded files, use this Python watchdog script:
from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class UploadHandler(FileSystemEventHandler): def on_created(self, event): if not event.is_directory: print(f"New file uploaded: {event.src_path}") # Add your processing logic here observer = Observer() observer.schedule(UploadHandler(), path='/ftp/uploads') observer.start()
- Set correct directory permissions (chmod 730 /ftp/uploads)
- Implement per-IP rate limiting
- Enable TLS encryption (consider using Let's Encrypt)
- Regularly audit uploaded files for malware
Verify the setup using this curl command:
curl -T testfile.txt ftp://user:pass@yourserver/ --ftp-create-dirs
Then attempt to list directory contents:
ftp> ls 550 Permission denied.
In data collection systems, IoT applications, or log aggregation scenarios, you often need an FTP server that accepts uploads while preventing clients from viewing directory contents. This write-only configuration is particularly useful when:
- Collecting sensitive customer uploads
- Building automated data pipelines
- Creating anonymous dropboxes
- Preventing information disclosure
vsftpd (Very Secure FTP Daemon) is ideal for this setup. Here's a complete configuration:
# /etc/vsftpd.conf listen=YES anonymous_enable=NO local_enable=YES write_enable=YES chroot_local_user=YES allow_writeable_chroot=YES hide_ids=YES dirlist_enable=NO download_enable=NO force_dot_files=YES
Key parameters explained:
dirlist_enable=NO
- Disables directory listingdownload_enable=NO
- Blocks file downloadshide_ids=YES
- Masks file ownership
For systems using Pure-FTPd, create this configuration:
# /etc/pure-ftpd/conf/NoDisplayFiles yes # /etc/pure-ftpd/conf/NoChmod yes # /etc/pure-ftpd/conf/NoRename yes # /etc/pure-ftpd/conf/CustomerProof yes
Verify the setup using this Python test script:
from ftplib import FTP def test_ftp(): try: ftp = FTP('your.server.com') ftp.login('username', 'password') # Test upload with open('test.txt', 'wb') as f: ftp.storbinary('STOR test.txt', f) # Test listing (should fail) try: print(ftp.nlst()) print("FAIL: Directory listing succeeded") except: print("PASS: Directory listing blocked") ftp.quit() except Exception as e: print(f"Error: {str(e)}") test_ftp()
- Implement rate limiting to prevent abuse
- Set up proper disk quotas
- Regularly monitor upload directories
- Consider using FTPS (FTP over SSL) for encryption
For more control, you can build a minimal FTP server in Python:
from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer class WriteOnlyHandler(FTPHandler): def on_file_received(self, file): # Process uploaded files here pass def ftp_LIST(self, path): return "" authorizer = DummyAuthorizer() authorizer.add_user("user", "password", "/upload/dir", perm="w") handler = WriteOnlyHandler handler.authorizer = authorizer server = FTPServer(("0.0.0.0", 21), handler) server.serve_forever()