In a recent project, I encountered the challenge of replacing an aging SFTP server while ensuring zero disruption to clients. The key requirement was that clients shouldn’t need to change any configurations—they should be able to connect to the new server as if nothing had changed. This meant that the new solution had to mimic the old one in every critical way: from IP address and host keys to settings and user experience.

Migrating from a legacy, single-instance SFTP server to a modern, high-availability solution like FileMage added complexity. I needed a way to thoroughly test the migration—ensuring that everything was configured correctly—without touching the live environment or interrupting client connections.

In this post, I’ll walk through a method I developed for testing the migration of an SFTP service by “tricking” the client into believing the new server is the same one it has always connected to. While Docker simplifies and standardizes the testing process, this approach can be used with any SFTP client by manually modifying the known_hosts file.

Creating a Repeatable Testing Environment

Since Docker was already part of our development environment, we leveraged it to create a repeatable test environment. The first step was to create a Docker container with the SFTP client.

FROM ubuntu:latest

ARG USERNAME=sftp
ARG USERID=1000
ARG GROUPID=1000

RUN apt-get update && apt-get install --assume-yes --no-install-recommends openssh-sftp-server

RUN groupadd --gid ${GROUPID} ${USERNAME} \
    || echo "Group '${GROUPID}' already exists."

RUN useradd --gid ${GROUPID} ${USERNAME} \
    || echo "User '${USERNAME}' already exists."

ENTRYPOINT ["/usr/bin/sftp"]
CMD ["-h"]

This Dockerfile creates a minimal image with the SFTP client installed and ensures that the user created inside the container matches the host user’s UID and GID. This is critical for maintaining proper file permissions in the .ssh directory.

Next, we used GNU Make to streamline the process with repeatable commands:

IMAGE ?= sftp-client
VERSION ?= latest
TAG ?= $(IMAGE):$(VERSION)
CONTAINER_RUNTIME ?= docker
USERNAME ?= $(shell whoami)
USERID ?= $(shell id -u $(USERNAME))
GROUPID ?= $(shell id -g $(USERNAME))

VOLUMES=--volume $(PWD)/ssh:/home/$(USERNAME)/.ssh \
     --volume $(PWD):/home/$(USERNAME)

SFTP_PORT ?= 22
SFTP_HOST ?=
SFTP_USER ?=

.PHONY: permissions
permissions:
 @chmod 700 ssh
 @chmod 600 ssh/config
 @chown $(USERID):$(GROUPID) ssh
 @chown $(USERID):$(GROUPID) ssh/config

.PHONY: build
build: permissions
 @$(CONTAINER_RUNTIME) build \
  --build-arg USERNAME=$(USERNAME) \
  --build-arg USERID=$(USERID) \
  --build-arg GROUPID=$(GROUPID) \
  -f Dockerfile \
  -t $(TAG) \
  .

.PHONY: run
run: permissions
 @$(CONTAINER_RUNTIME) run \
  --user $(USERID):$(GROUPID) \
  --interactive \
  --tty \
  --rm \
  --name $(IMAGE) \
  --workdir="/home/$(USERNAME)" \
  $(VOLUMES) $(TAG) \
  -P $(SFTP_PORT) $(SFTP_USER)@$(SFTP_HOST)

.PHONY: shell
shell: permissions
 @$(CONTAINER_RUNTIME) run \
  --user $(USERID):$(GROUPID) \
  --interactive \
  --tty \
  --rm \
  --name $(IMAGE) \
  --workdir="/home/$(USERNAME)" \
  --entrypoint /bin/bash \
  $(VOLUMES) $(TAG)

In the run command, we mount the ssh directory from the current working directory into the container. This is where the known_hosts file is stored, allowing us to test connections without immediately migrating the IP address. The container runs under the user created in the Dockerfile, maintaining proper permissions for SSH keys.

Next, we configure the SFTP client to ensure the hostnames or IPs are human-readable in the known_hosts file by disabling hashing in the ssh/config file:

HashKnownHosts No

How to Test the Migration

With this setup, the testing environment can be distributed to developers, QA, and anyone involved in the migration process. Here’s how to use it to verify the migration:

  1. Run the SFTP client to connect to the old server and accept the server’s host key.
make run SFTP_HOST=x.x.x.x SFTP_USER=username

Output:

The authenticity of host 'x.x.x.x (x.x.x.x)' can't be established.
ED25519 key fingerprint is SHA256:........................................
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'x.x.x.x' (ED25519) to the list of known hosts.
username@x.x.x.x's password:
Connected to x.x.x.x.
sftp>
  1. Disconnect from the old SFTP server.
  2. Verify the ssh/known_hosts was created and has the entry for the old SFTP server.
cat ssh/known_hosts

Output:

x.x.x.x ssh-ed25519 AAAA..........................
x.x.x.x ssh-rsa AAAA..............................
x.x.x.x ecdsa-sha2-nistp256 AAAA..................
  1. Update the IP address in known_hosts to point to the new SFTP server:
sed -i 's/^x.x.x.x/y.y.y.y/g' ssh/known_hosts

Output:

y.y.y.y ssh-ed25519 AAAA..........................
y.y.y.y ssh-rsa AAAA..............................
y.y.y.y ecdsa-sha2-nistp256 AAAA..................
  1. Connect to the new SFTP endpoint:
make run SFTP_HOST=y.y.y.y SFTP_USER=username

Output:

sftp>

If the SSH host keys were migrated correctly, you shouldn’t receive any authenticity warnings or be prompted to accept the host key.

Conclusion

While additional testing may be required, this method provides a reliable and repeatable environment to verify the new SFTP solution is ready to replace the old server without impacting clients. With this in place, the migration should go smoothly and without any surprises.