SFTP Server — SFTPGo on Ubuntu (vm-sftp-prod-001)
Overview
Azure VM running SFTPGo as the managed SFTP service for eSHARS data exchange with external partners and customers. Fronted by a Standard Load Balancer for inbound traffic. The VM has no direct public IP — all access is through the LB frontend.
Migration in progress: Clients currently connect to the legacy VM (
vm-eshars-sftp/40.77.98.52). The cutover plan is to reassign40.77.98.52to the LB frontend once testing is complete. See Cutover Plan below.
Architecture
Internet (port 22) │ ▼┌─────────────────────────────┐│ lb-sftp-prod (Standard) ││ Frontend: pip-sftp-prod ││ 130.131.245.25 ││ ││ Rule: TCP 22 → 22 ││ Probe: TCP 22 ││ Outbound: 1024 SNAT ports │└────────────┬────────────────┘ │ ▼┌─────────────────────────────────────────┐│ rg-eshars-prod ││ ││ ┌───────────────────────────────────┐ ││ │ vm-sftp-prod-001 (Standard_B2s) │ ││ │ Ubuntu 24.04 LTS │ ││ │ Private IP: 10.0.0.15 (Static) │ ││ │ │ ││ │ ┌─────────────────────────────┐ │ ││ │ │ SFTPGo │ │ ││ │ │ Port: 22 │ │ ││ │ │ Users DB: /var/lib/sftpgo/ │ │ ││ │ │ Data: /srv/sftpgo/data │ │ ││ │ │ Backups: /srv/sftpgo/backups│ │ ││ │ └─────────────────────────────┘ │ ││ │ │ ││ │ ┌─────────────────────────────┐ │ ││ │ │ fail2ban │ │ ││ │ │ sshd jail: 3 tries → 24h │ │ ││ │ │ sshd-ddos: 10/min → 1h │ │ ││ │ └─────────────────────────────┘ │ ││ │ │ ││ │ Disks: │ ││ │ /dev/sdb (30GB) — OS │ ││ │ /dev/sda (128GB) — SFTPGo data │ ││ │ └─ sda1 → /srv/sftpgo │ ││ └───────────────────────────────────┘ ││ ││ nsg-vm-sftp-prod-001 ││ ├─ Allow TCP 22 inbound (*) pri 100 ││ ├─ Allow TCP 2222 inbound (VNet) pri 110││ └─ Deny all inbound pri 4000 │└─────────────────────────────────────────┘Resource Inventory (prod)
| Resource | Name | Notes |
|---|---|---|
| Resource Group | rg-eshars-prod | Shared prod RG |
| Virtual Machine | vm-sftp-prod-001 | Standard_B2s, Central US |
| NIC | vm-sftp-prod-001-temp-nic | Name is legacy — rename in IaC |
| Private IP | 10.0.0.15 | Static |
| Load Balancer | lb-sftp-prod | Standard SKU |
| LB Frontend IP | pip-sftp-prod | 130.131.245.25, Standard, Static |
| LB Backend Pool | be-sftp | Contains vm-sftp-prod-001 NIC |
| LB Rule | rule-sftp-22 | TCP 22→22, outbound SNAT disabled |
| LB Probe | probe-sftp-22 | TCP 22 |
| LB Outbound Rule | outbound-sftp | All protocols, 1024 SNAT ports |
| NSG | nsg-vm-sftp-prod-001 | See rules above |
| OS Disk | — | 30GB |
| Data Disk | /dev/sda (sda1) | 128GB ext4, UUID 2bc4f457-e440-4d09-b7d8-30e6bfde287e |
| Defender for Servers | P1 | Enabled on subscription |
VM Configuration
OS & Software
- OS: Ubuntu 24.04 LTS (
6.17.0-1010-azureas of 2026-04-09) - SFTP daemon: SFTPGo (systemd service:
sftpgo) - Brute-force protection: fail2ban 1.0.2
SFTPGo
- Config:
/etc/sftpgo/sftpgo.json - User database:
/var/lib/sftpgo/sftpgo.db(SQLite, on OS disk) - Users base dir:
/srv/sftpgo/data(on data disk) - Backups path:
/srv/sftpgo/backups(on data disk) - Admin port: SFTPGo web admin (check config for port)
- SSH port: 22
Disk Layout
| Mount | Device | Size | Filesystem | Notes |
|---|---|---|---|---|
/ | /dev/sdb1 | 30GB | ext4 | OS disk |
/srv/sftpgo | /dev/sda1 | 128GB | ext4 | Data disk — all user files |
/boot | /dev/sdb16 | 913MB | ext4 | |
/boot/efi | /dev/sdb15 | 106MB | vfat |
Critical: /etc/fstab mounts /srv/sftpgo by UUID. Do NOT change it to a device name — Azure may reassign device letters on resize/reboot. To verify the current UUID on the VM, check grep /srv/sftpgo /etc/fstab and confirm it with blkid or lsblk -f.
fail2ban (/etc/fail2ban/jail.local)
[DEFAULT]bantime = 1hfindtime = 10mmaxretry = 5backend = systemd
[sshd]enabled = trueport = 22logpath = %(sshd_log)smaxretry = 3bantime = 24h
[sshd-ddos]enabled = trueport = 22filter = sshdlogpath = %(sshd_log)smaxretry = 10findtime = 1mbantime = 1hAdmin SSH
Port 2222 is open to VNet only (AllowSSHAdmin NSG rule, priority 110). Use Bastion or a jump host on the VNet for administrative access.
Cutover Plan
Clients currently connect to the legacy VM at 40.77.98.52 (vm-hisd-sftp-ip, attached to vm-eshars-sftp896 NIC in vm-eshars-sftp_group).
Cutover steps (zero client changes required)
- Confirm testing complete on
130.131.245.25 - Detach
vm-hisd-sftp-ipfrom legacy VM NIC:Terminal window az network nic ip-config update \--resource-group vm-eshars-sftp_group \--nic-name vm-eshars-sftp896 \--name ipconfig1 \--remove publicIpAddress - Attach
vm-hisd-sftp-ipto LB frontend:Terminal window az network lb frontend-ip update \--resource-group rg-eshars-prod \--lb-name lb-sftp-prod \--name fe-sftp \--public-ip-address vm-hisd-sftp-ip - Clients now hit
40.77.98.52→ LB →vm-sftp-prod-001. No changes needed on client side.
Rollback (if cutover fails, ~90 seconds)
# Step 1 — restore LB to its own IPaz network lb frontend-ip update \ --resource-group rg-eshars-prod \ --lb-name lb-sftp-prod \ --name fe-sftp \ --public-ip-address pip-sftp-prod
# Step 2 — reattach old IP to legacy VM NICaz network nic ip-config update \ --resource-group vm-eshars-sftp_group \ --nic-name vm-eshars-sftp896 \ --name ipconfig1 \ --public-ip-address vm-hisd-sftp-ip40.77.98.52 is never deleted — it stays allocated in the subscription throughout, so rollback is always available.
Security Posture
| Control | Status | Notes |
|---|---|---|
| No direct public IP on VM | ✅ | Removed — LB only |
| fail2ban | ✅ | 3 failures = 24h ban |
| Defender for Servers P1 | ✅ | Subscription-level |
| NSG deny-all default | ✅ | Priority 4000 |
| SSH key auth only | ❌ | Password auth still enabled — migration in progress |
| IP allowlisting | ❌ | Not feasible — customers connect from dynamic IPs |
Password auth migration plan
- New customers: provision with SSH keypair from day one
- Existing customers: reach out to add public key, flip to key-only per account
- Final step: disable
PasswordAuthenticationglobally in sshd_config once all accounts migrated
Known Issues / Technical Debt
- NIC name —
vm-sftp-prod-001-temp-nichas a legacy “temp” name. Rename via IaC. - Password auth enabled — see migration plan above.
- Legacy VMs —
HISD-SFTP,vm-hisd-sftp, andvm-eshars-sftpin prod still exist. Decommission after cutover is validated. - Basic LB —
lb-eshars-prod(Basic SKU, empty) exists inrg-eshars-prod. Basic LB is retired — decommission or upgrade. - IaC — this VM and LB were provisioned manually. IaC to be created (see below).
IaC Backlog
The following resources need to be codified in OpenTofu:
-
vm-sftp-prod-001— VM, NIC, NSG, OS disk -
lb-sftp-prod— LB, frontend IP, backend pool, rule, probe, outbound rule -
pip-sftp-prod— Standard static public IP - Data disk (
sda) attachment and fstab config - fail2ban provisioning (cloud-init or custom script extension)
- SFTPGo installation and config
- Defender for Servers P1 enablement
Target: infrastructure/hisd-sftp/ (module already exists, needs populating).
Operations
Check SFTPGo status
systemctl status sftpgoCheck fail2ban bans
fail2ban-client status sshdfail2ban-client status sshd-ddosUnban an IP
fail2ban-client set sshd unbanip <ip>View SFTPGo logs
journalctl -u sftpgo -fMount check after reboot
df -h | grep srv# Should show /dev/sda1 mounted at /srv/sftpgo