FTP servers are a surprisingly effective way to move data around in Kubernetes, especially when you’re dealing with legacy systems or partners who don’t have direct API access.

Let’s get an vsftpd server up and running, and make sure its data sticks around even if the pod restarts.

First, we need a ConfigMap for vsftpd’s configuration. This is where we’ll set up anonymous access and passive mode.

apiVersion: v1
kind: ConfigMap
metadata:
  name: vsftpd-config
data:
  vsftpd.conf: |
    anonymous_enable=YES
    local_enable=NO
    write_enable=YES
    anon_upload_enable=YES
    anon_mkdir_write_enable=YES
    chroot_local_user=NO
    secure_chroot_dir=/var/run/vsftpd/empty
    pam_service_name=vsftpd
    rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
    rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
    ssl_enable=YES
    allow_anon_ssl=YES
    force_local_data_ssl=NO
    force_local_logins_ssl=YES
    ssl_tlsv1=YES
    ssl_sslv2=NO
    ssl_sslv3=NO
    require_ssl_reuse=NO
    ssl_ciphers=HIGH
    pasv_enable=YES
    pasv_min_port=40000
    pasv_max_port=50000

This config enables anonymous access, allows uploads and directory creation for anonymous users, and crucially, sets up passive mode with a defined port range (40000-50000). This port range is essential for clients behind firewalls to connect. We’re also enabling SSL for security, though with self-signed certs for simplicity here.

Next, we’ll define a PersistentVolumeClaim to hold our FTP data. This ensures that any files uploaded will survive pod restarts.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vsftpd-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

This PVC requests 5Gi of storage that can be mounted as read-write by a single node. Kubernetes will provision a PersistentVolume (PV) to fulfill this request, typically using your cluster’s default storage class.

Now, let’s create the Deployment for our vsftpd server. This will include the vsftpd container, mount our ConfigMap, and attach the PersistentVolume.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vsftpd-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vsftpd
  template:
    metadata:
      labels:
        app: vsftpd
    spec:
      containers:
      - name: vsftpd
        image: stilliard/vsftpd # A common vsftpd image
        ports:
        - containerPort: 21  # FTP control port
        - containerPort: 40000 # Passive mode port range start
        - containerPort: 50000 # Passive mode port range end
        volumeMounts:
        - name: config-volume
          mountPath: /etc/vsftpd
        - name: data-volume
          mountPath: /var/ftp
      volumes:
      - name: config-volume
        configMap:
          name: vsftpd-config
      - name: data-volume
        persistentVolumeClaim:
          claimName: vsftpd-data-pvc

The Deployment specifies one replica, uses the stilliard/vsftpd image, and exposes ports 21 (control) and the passive mode range (40000-50000). The volumeMounts section connects the vsftpd-config ConfigMap to /etc/vsftpd inside the container and the vsftpd-data-pvc to /var/ftp, which is where vsftpd typically stores its data.

Finally, we expose the FTP service to the outside world with a Service. We need to expose both the control port and the passive port range.

apiVersion: v1
kind: Service
metadata:
  name: vsftpd-service
spec:
  selector:
    app: vsftpd
  ports:
  - name: control
    protocol: TCP
    port: 21
    targetPort: 21
  - name: passive-1
    protocol: TCP
    port: 40000
    targetPort: 40000
  - name: passive-2
    protocol: TCP
    port: 40001
    targetPort: 40001
  # ... repeat for ports 40002 to 50000 ...
  - name: passive-1000
    protocol: TCP
    port: 50000
    targetPort: 50000
  type: LoadBalancer

Here, we create a LoadBalancer type service. This will provision an external IP address and forward traffic to our vsftpd pods. Crucially, we’re mapping all ports from 40000 to 50000 to the corresponding targetPorts on the pod. This allows clients to establish passive FTP connections.

With these resources applied (kubectl apply -f ...), you’ll have an anonymous FTP server running in Kubernetes. You can then connect using an FTP client to the external IP address provided by the LoadBalancer service, using anonymous as the username and any email address as the password. Files uploaded will be stored in the vsftpd-data-pvc and will persist across pod restarts.

If you find yourself needing to restrict access or add user authentication, you’ll need to modify the vsftpd.conf in the ConfigMap, potentially change local_enable to YES, and manage user credentials, which often involves mounting a file with hashed passwords or integrating with an external authentication source.

The next hurdle you’ll likely encounter is managing the IP addresses for passive FTP connections when running vsftpd behind a Kubernetes Service of type NodePort or LoadBalancer with dynamic IPs, as the vsftpd server needs to advertise the correct external IP for passive mode.

Want structured learning?

Take the full Ftp course →