THB-P-SFTP-WIN Cerberus FTP Server Upgrade
Upgrade Cerberus FTP Server on THB-P-SFTP-WIN, the Health Align production SFTP server, with a rollback ready and partner traffic checked afterwards.
Use this runbook when:
- The Cerberus dashboard’s Vulnerability Assessment
shows
Vulnerabilities: Detected - Help → Check for Update offers a new release
- The licence has been renewed and a newer version is covered
The upgrade takes about 2 minutes of SFTP downtime: the installer stops the service, replaces the program files and starts it again. Users and settings are kept. It rarely needs a reboot.
The server at a glance
See THB-P-SFTP-WIN Storage Cleanup for the VM, drives and scheduled tasks, and Access for RDP and Run Command. The upgrade itself needs RDP; the checks below work through Run Command.
| Service | Cerberus FTP Server, runs as the local account
.\Cerberus, startup Automatic |
| Program files | C:\Program Files\Cerberus LLC\Cerberus FTP Server\
(CerberusGUI.exe -Service) |
| Config and state | C:\ProgramData\Cerberus LLC\Cerberus FTP Server\
(users, groups, listeners, settings, certificates\,
publickeys\) |
| Downloaded installers | C:\ProgramData\Cerberus LLC\Cerberus FTP Server\installers\
(CerberusUpdate_amd64_<version>.exe). Older ones stay
here and serve as rollback installers. |
| Logs | ...\Cerberus FTP Server\log\server.*.log. Six 5 MB
files that roll every 7–8 hours, so only ~4 days of history. |
| Listeners | 22 (SFTP, the only one the NSG opens to partners), 21 and 990
(FTPS), 8443 (web admin, localhost), 10001 (SOAP), 58000 (internal,
127.0.0.1 only) |
| Disks | OS disk thbpsftpwin-osdisk-20250924-203359
(C:), data disk
thbpsftpwin-datadisk-001-20250924-203359
(E:) |
1. Prepare (days before)
Read the release notes
Read the release notes for every version between the installed one and the target. Look for:
- CVEs and their exposure. Only port 22 is reachable from the internet. TLS fixes matter less than SSH or local-privilege fixes.
- Removed SSH algorithms (ciphers, MACs, host keys, key exchange). See Old SSH clients before agreeing to any removal.
- “Installs older than X must update manually”, a licence agreement (EULA or MSA) change, or other notes about the upgrade path. These mean the in-app updater stops partway (see Upgrade).
- Reporting, FIPS or log-format changes. This server doesn’t use reporting or FIPS mode.
Check the licence
The dashboard’s Current license expiration must be
in the future, and the licence must cover the target version. Renewals
go through the Renew button, which opens the Cerberus
customer portal (portal.cerberusftp.com). The portal can be
down (Cloudflare 522). Try again later, or contact Cerberus support
(support.cerberusftp.com).
Pick the window
Partner pollers connect around the clock (~360 connections an hour overnight), so no hour is empty. Polling clients retry, so a short outage costs them a few missed polls, not files.
| Hours (CT) | Traffic |
|---|---|
| 21:00–23:00 | Quietest. Best window, while someone is still around to fix problems. |
| 07:00–09:00 | Avoid. Our gcp_scan (07:00) and
gcp_centene (08:00) Cloud Run jobs. 08:00 is the busiest
hour on the server. |
| 11:00–15:00 | Daytime peak |
Prefer Tuesday to Thursday. Weekends aren’t quieter (the meal vendors run every day), and a Friday problem sits over the weekend. To recheck the pattern before choosing, run Traffic by hour.
Announce the outage with Execute a Planned Outage, so Accounts can decide whether partners need notice.
2. Back up (right before the window)
Cerberus backup (RDP)
- Open PowerShell as administrator and pick the
backup
<stamp>once. Write it down: the zip, the folder copy, the snapshots and the cleanup all use this same value.
$stamp = Get-Date -Format yyyyMMdd-HHmm
$stamp- In the Cerberus admin UI, use Tools → Backup Users and
Settings. Save the zip to
E:\ascerberus_server_backup_<stamp>.zip. - In the same PowerShell window, copy the whole config folder too:
robocopy "C:\ProgramData\Cerberus LLC\Cerberus FTP Server" "E:\cerberus-backup-$stamp" /E /XD log tempFiles /R:1 /W:1
# robocopy exit codes 8 and above mean some files failed to copy.
if ($LASTEXITCODE -ge 8) { throw "Config backup failed (robocopy exit $LASTEXITCODE). Fix it before taking snapshots." }Keep both on E: and don’t copy them to
S:. They hold every partner account’s password
hash and SSH key. The E: snapshot below is the off-VM
copy.
SSH host key (your laptop)
Record the host key fingerprint that partners have pinned, to compare after the upgrade:
ssh-keyscan -T 10 -p 22 13.77.105.190 2>/dev/null | ssh-keygen -lf -On 2026-10-28 it was
3072 SHA256:aBiM6rQAaJkw4c4FtiLg75hoGEQe8bWIlVEPcgs/Bmo (RSA),
the only host key offered.
Disk snapshots (your laptop)
Take them after the Cerberus backup, so the E: snapshot
contains it. Snapshots don’t interrupt SFTP.
Use the backup’s <stamp>
(yyyyMMdd-HHmm) in the snapshot names, and never reuse a
name. az snapshot create with an existing name
overwrites that snapshot, so retrying after a failed
upgrade would replace the good rollback point with the broken state.
az snapshot create -g THB-PROD-SFTP-RG -n thbpsftpwin-os-pre-cerberus-<target-version>-<stamp> --source thbpsftpwin-osdisk-20250924-203359 --incremental trueaz snapshot create -g THB-PROD-SFTP-RG -n thbpsftpwin-data-pre-cerberus-<target-version>-<stamp> --source thbpsftpwin-datadisk-001-20250924-203359 --incremental trueThen stop anyone from downloading them. They’re only needed for a restore inside Azure:
az snapshot update -g THB-PROD-SFTP-RG -n thbpsftpwin-os-pre-cerberus-<target-version>-<stamp> --network-access-policy DenyAll --public-network-access Disabledaz snapshot update -g THB-PROD-SFTP-RG -n thbpsftpwin-data-pre-cerberus-<target-version>-<stamp> --network-access-policy DenyAll --public-network-access DisabledS: (the hasftp Azure file share) is not on
either disk. The upgrade doesn’t touch it.
3. Upgrade
- In the admin UI, open Connections and let any active transfers finish.
- Open Help → Check for Update and confirm the
offered version is the target. Click Update. This
downloads the installer to the
installers\folder and starts it. - If a dialog says the End User License Agreement has been
updated and asks you to run the full installer, click
OK. The service is still running the old version at
this point. Then:
Check the downloaded installer’s signature in PowerShell. Status must be
Validand the signerRedwood Software, Inc.:Get-AuthenticodeSignature "C:\ProgramData\Cerberus LLC\Cerberus FTP Server\installers\CerberusUpdate_amd64_<version>.exe" | Format-List Status, SignerCertificateRun that
.exeas administrator.
- In the installer:
- Service credentials: keep Use Current:
.\Cerberusand click Next. Don’t change anything under Advanced: that account owns the file permissions. - Install path: it’s locked to the existing path. Tick I agree to the license terms and click Next.
- It installs prerequisites (Visual C++ redistributable), then stops the service. The outage starts here.
- Service credentials: keep Use Current:
- On successfully installed, click Finish. Reboot only if the installer asks.
If the updater isn’t offered, or fails, download the 64-bit installer
from cerberusftp.com/download
instead. Before running it, check it the same way as above: the
signature Status must be Valid, the signer
Redwood Software, Inc., and the version the target:
$i = Get-Item "<path to downloaded installer>.exe"
$i.VersionInfo.FileVersion
Get-AuthenticodeSignature $i.FullName | Format-List Status, SignerCertificateThen exit the admin UI with File → Exit, run
Stop-Service "Cerberus FTP Server", and run the installer
as administrator.
4. Verify
From your laptop
Save this as cerberus-check.ps1 and run it with Run
Command. Set $since to the time the upgrade finished:
$since = '<upgrade finish time, yyyy-MM-dd HH:mm:ss>'
$t = [datetime]::MinValue
if (-not [datetime]::TryParseExact($since, 'yyyy-MM-dd HH:mm:ss', [Globalization.CultureInfo]::InvariantCulture, 'None', [ref]$t) -or $t -gt (Get-Date)) { throw 'Set $since to the upgrade finish time first (yyyy-MM-dd HH:mm:ss, not in the future).' }
$b = "$env:ProgramData/Cerberus LLC/Cerberus FTP Server"
$svc = Get-CimInstance Win32_Service -Filter "Name='Cerberus FTP Server'"
"service: $($svc.State) / $($svc.StartMode) / $($svc.StartName)"
"version: " + (Get-Item (($svc.PathName -replace '"','') -split ' -')[0].Trim()).VersionInfo.FileVersion
$pids = (Get-Process | Where-Object ProcessName -like 'Cerberus*').Id
"listening: " + ((Get-NetTCPConnection -State Listen | Where-Object { $pids -contains $_.OwningProcess } | Select-Object -ExpandProperty LocalPort -Unique | Sort-Object) -join ',')
$l = Get-Content (Get-ChildItem "$b/log" -Filter 'server*.log' | Sort-Object LastWriteTime | Select-Object -Last 1).FullName | Where-Object { $_.Length -gt 21 -and $_.Substring(1,19) -gt $since }
"logins since: " + (($l | Where-Object { $_ -match "Signature verified with public key presented by user '([^']+)'" } | ForEach-Object { $matches[1] } | Group-Object | ForEach-Object { "$($_.Name):$($_.Count)" }) -join ' ')
$known = @(
'Unable to add to Windows Firewall Exception list: Access is denied'
"Unable to open file handle to 'C:\\FTP\\[^']*\\incoming\\[^']*' : The system cannot find the file specified"
"\]:imperial - Unable to (access local directory '[^']*\\_gsdata_'|open directory handle to '/incoming/_gsdata_')"
"Public key 1 does not match key presented by user 'wellcare_icon'"
) -join '|'
"unexpected errors: " + (($l | Where-Object { $_ -match ' ERROR ' -and $_ -notmatch $known } | Select-Object -First 5) -join ' || ')
foreach ($t in 'MoveFTPFiles','ArchiveMealReferrals') { $i = Get-ScheduledTaskInfo -TaskName $t; "{0}: last={1} result=0x{2:X}" -f $t, $i.LastRunTime.ToString('s'), $i.LastTaskResult }az vm run-command invoke -g THB-PROD-SFTP-RG -n THB-P-SFTP-WIN --command-id RunPowerShellScript --scripts "@cerberus-check.ps1" --query "value[0].message" -o tsvThen rerun the host key check:
ssh-keyscan -T 10 -p 22 13.77.105.190 2>/dev/null | ssh-keygen -lf -Expected:
- The host key fingerprint is unchanged. Partners and
our Cloud Run jobs pin it, so a new key breaks all of them, and we’d
only find out from the 07:00 job. If it changed, restore
certificates\fromE:\cerberus-backup-<stamp>\and restart the service, or roll back. - service is
Running / Auto / .\Cerberus, and version is the target. - listening shows the same ports as before:
21,22,990,8443,10001,58000. - logins since lists partners, including
wellcare_gafoods, which connects about once a minute with the oldest SSH client (see Old SSH clients). If it’s missing, roll back. - unexpected errors is empty. The script filters out
these known, harmless errors:
Unable to add to Windows Firewall Exception list: Access is denied, logged at startup. The service account can’t add firewall rules, and the existing allow rule forCerberusGUI.exealready covers the same path.- An occasional
Unable to open file handle to 'C:\FTP\...\incoming\...' : The system cannot find the file specified.\MoveFTPFilesmoved an upload toS:before the partner’s client opened it again. imperial:Unable to access local directory '...\_gsdata_', hourly. Its GoodSync client looks for a metadata folder that doesn’t exist.wellcare_icon:Public key 1 does not match key presented, nightly at 01:00. Its client offers a non-matching key first, then logs in with the right one.
- MoveFTPFiles shows result
0x0after its next 10-minute run.
In the admin UI
- Vulnerability Assessment: Current Version = Latest
Version, and
Vulnerabilities: None. This panel only covers known CVEs in the installed version. Algorithm warnings such as “SSH hmac-sha1 Mac is active” appear separately under System Messages and don’t affect it. On 2026-10-27 it showedNonewithhmac-sha1still enabled. Leave those warnings alone (see Old SSH clients). - Server Manager → Protocols → SSH SFTP: Mask Server Identification (SSH and HTTP/S) is still ticked. It hides the Cerberus version from anyone probing port 22.
- Spot-check User Manager: the user count, a couple of users’ folders and SSH keys.
The next morning
- Check that the 07:00 and 08:00 Cloud Run jobs
(
gcp_scan_cloud_run_user,gcp_centene_cloud_run_user) logged in and moved files. - Run the SHA-1 client script and check
that the other legacy-client accounts logged in. They connect far less
often than
wellcare_gafoods, so give them a day or two.
5. Roll back
Use the smallest step that fixes the problem:
- Settings lost or wrong. Use Tools →
Restore with the zip, or stop the service, copy
E:\cerberus-backup-<stamp>\back overC:\ProgramData\Cerberus LLC\Cerberus FTP Server\, and start the service again. - New version won’t run. Reinstall the previous
version from
installers\CerberusUpdate_amd64_<previous>.exe, then restore the settings as above. - The VM is broken. Create a disk from the OS snapshot and swap it in as the OS disk:
az disk create -g THB-PROD-SFTP-RG -n thbpsftpwin-osdisk-restored-<stamp> --source thbpsftpwin-os-pre-cerberus-<target-version>-<stamp>az vm stop -g THB-PROD-SFTP-RG -n THB-P-SFTP-WINaz vm update -g THB-PROD-SFTP-RG -n THB-P-SFTP-WIN --os-disk thbpsftpwin-osdisk-restored-<stamp>az vm start -g THB-PROD-SFTP-RG -n THB-P-SFTP-WINRun Verify again. Once it passes, the old OS disk is detached but still billed. Delete it:
az disk delete -g THB-PROD-SFTP-RG -n <old OS disk name from the table>Then, in a PR, update the OS disk name in the table and in the snapshot commands, so the next upgrade snapshots the live disk.
6. Clean up
After about a week of normal partner traffic, delete the rollback copies. They hold partner credentials, and the snapshots cost money.
- The snapshots, from your laptop:
az snapshot delete -g THB-PROD-SFTP-RG -n thbpsftpwin-os-pre-cerberus-<target-version>-<stamp>az snapshot delete -g THB-PROD-SFTP-RG -n thbpsftpwin-data-pre-cerberus-<target-version>-<stamp>- The Cerberus backups on
E:, on the VM. Keep only the newest pair if you want a config fallback for the next upgrade:
Remove-Item "E:\cerberus_server_backup_<stamp>.zip", "E:\cerberus-backup-<stamp>" -Recurse -ForceReference
Old SSH clients
Some partners still negotiate old algorithms. On 2026-10-27 these
accounts used hmac-sha1: wellcare_gafoods
(client SSH-2.0-SecureBlackbox, with
aes128-cbc + ssh-rsa, on every session),
simpra, Aetna and
abilis_healthaxis. Every other account uses SHA-2, CTR, GCM
or ChaCha20. Don’t disable hmac-sha1,
aes128-cbc or the ssh-rsa host key
algorithm, even if the dashboard recommends it, and don’t take
an upgrade that removes any of them, until those partners have updated
their clients.
To see which accounts use SHA-1 now:
$sha1 = @{}; $user = @{}
Get-ChildItem "$env:ProgramData/Cerberus LLC/Cerberus FTP Server/log" -Filter 'server*.log' | ForEach-Object { Get-Content $_.FullName } | ForEach-Object {
if ($_ -match '\[\s*(\d+)\] - \[[^\]]*\]:([^ ]*) - (.*)$') {
# Copy out of $matches first: the -match below overwrites it.
$id = $matches[1]; $u = $matches[2]; $msg = $matches[3]
if ($msg -like 'Agreed upon*') { $sha1[$id] = ($msg -match "hmac-sha1, none'") }
if ($u -and -not $user.ContainsKey($id)) { $user[$id] = $u }
}
}
$sha1.Keys | Where-Object { $sha1[$_] } | ForEach-Object { $user[$_] } | Group-Object | ForEach-Object { "$($_.Name): $($_.Count) sha1 sessions" }Session IDs restart after a service restart, so run this on logs from a single run of the service. Right after an upgrade, wait a few hours.
Traffic by hour
Connections and file opens per hour (CT) across the retained logs,
excluding lynx_test, which polls every few seconds:
$rx = '^\[\d{4}-\d\d-\d\d (\d\d):\d\d:\d\d\]:\s*\w+ \[\s*\d+\] - \[[^\]]+\]:([^ ]*) - (Incoming connection|File open)'
$rows = Get-ChildItem "$env:ProgramData/Cerberus LLC/Cerberus FTP Server/log" -Filter 'server*.log' | ForEach-Object { Get-Content $_.FullName } | ForEach-Object { if ($_ -match $rx -and $matches[2] -ne 'lynx_test') { [pscustomobject]@{ h = [int]$matches[1]; k = $matches[3] } } }
0..23 | ForEach-Object { $h = $_; $x = $rows | Where-Object h -eq $h; '{0:D2} conns={1,5} opens={2,5}' -f $h, ($x | Where-Object k -eq 'Incoming connection').Count, ($x | Where-Object k -eq 'File open').Count }Gotchas
- The in-app updater stops at a licence agreement
change. The dialog says to run the full installer, which it has
already downloaded to
installers\. Nothing has changed at that point. - The log format changed in 2026.x. Session IDs are
padded with spaces (
[ 0]). Match them with\[\s*\d+\]. admin - Connection timed outin the log is an idle admin-UI session, not a partner.- Automatic IP Blocking is off. If you turn it on, exempt the partner IPs first: pollers connect about once a minute and can get blocked. The NSG already limits port 22 to partner IPs.
- Expired
server.pem. The TLS certificate is a self-signedCN=HAWEB01cert that expired on 2025-11-14. It only affects FTPS and HTTPS, which the NSG blocks from the internet. The SFTP host keys are separate and valid until 2043. - The dashboard shows Host
HAWEB01. That’s the Windows computer name kept from the backup the VM was restored from. It’s expected.