7 minutes
Written: 2025-05-11 17:44 +0000
Simplified SSH access via container VPNs
This post is part of the Remote Usage series.
Leveraging containers for streamlined remote interactive and mounted file access to intranet machines
Background
Most academic or corporate networks gate access to internal resources (like clusters or services) via their local intranet, requiring a VPN connection (e.g., using Cisco AnyConnect, FortiGate). Connecting to such a VPN on a personal machine typically routes all your internet traffic through that VPN. This is useful for certain tasks like accessing paywalled journal articles via university subscriptions, but it becomes problematic when:
- You only want some of your traffic to go through the VPN.
- You need to connect to multiple distinct services, each behind its own separate VPN. Managing these simultaneously or switching between them on a host system is often cumbersome or impossible.
These considerations not only affect interactive SSH sessions but also complicates using tools for remote file management or other services that need to operate through these VPNs.
Tools
We’ll be using Podman with OpenConnect and OpenSSH to build a VPN jumphost for
simplified ProxyJump
configurations and rclone for SFTP mounting.
Normal usage
To recap, the standard workflow, for say, an anyconnect
VPN is as follows:
1sudo openconnect $VPN -u $VUSER --useragent AnyConnect
2# Another terminal
3ssh $LMACHINE
Where LMACHINE
is normally only accessible from the intranet, access to which
is provided by VPN
authenticated for VUSER
. sudo
is there since the
networking of the current machine, say CMACHINE
is affected. This works, but
as noted has the side effect of routing the entire machine’s traffic
through the VPN. Trying to manage multiple such VPNs is difficult.
Caveats
- Full Network Routing
- Your entire machine’s internet usage goes through the VPN.
- Multiple VPNs
- Switching between VPNs is slow, and running multiple simultaneously is generally not feasible.
- Streamlining Access
- Even with one VPN, accessing many internal machines via direct SSH can be made more convenient with a jumphost and SSH’s ProxyJump feature.
Container setup
Containerization offers a powerful solution by isolating the VPN connection within a dedicated environment, leaving your host system’s networking untouched. This container can then act as a secure SSH jumphost.
A basic container using an Alpine base with a non-root user for this is basically:
1FROM alpine:latest
2
3LABEL maintainer="rgoswami[at]ieee[dot]org"
4LABEL description="Alpine jumphost: OpenConnect (interactive) as main process, OpenSSH server for ProxyJump."
5
6ARG SSH_USER_NAME=jumphostuser
7# It's safer to pass the public key content at build time than to have a default here that might be forgotten.
8ARG USER_PUBLIC_KEY
9ARG PASS=nothing
10
11# Install necessary packages
12RUN apk add --no-cache \
13 tini \
14 openconnect \
15 openssh \
16 openssh-server \
17 bash \
18 ca-certificates
19
20# Create a non-root user for SSH connections INTO the container
21# The password isn't really ever required, but causes an error without it
22RUN adduser -g "${SSH_USER_NAME}" -D -s /bin/bash "${SSH_USER_NAME}" && \
23 echo "${SSH_USER_NAME}:$(openssl passwd -1 $PASS)" | chpasswd
24
25# Setup SSH for this user using the public key provided at build time
26RUN mkdir -p "/home/${SSH_USER_NAME}/.ssh" && \
27 chmod 700 "/home/${SSH_USER_NAME}/.ssh" && \
28 echo "${USER_PUBLIC_KEY}" > "/home/${SSH_USER_NAME}/.ssh/authorized_keys" && \
29 chmod 600 "/home/${SSH_USER_NAME}/.ssh/authorized_keys" && \
30 chown -R "${SSH_USER_NAME}:${SSH_USER_NAME}" "/home/${SSH_USER_NAME}/.ssh"
31
32# Configure SSHD for security and ProxyJump functionality
33RUN sed -i 's/^#?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && \
34 sed -i 's/^#?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \
35 sed -i 's/^#?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
36 \
37 # Robustly set AllowTcpForwarding to yes
38 sed -i '/^#\?AllowTcpForwarding.*/d' /etc/ssh/sshd_config && \
39 echo "AllowTcpForwarding yes" >> /etc/ssh/sshd_config && \
40 \
41 sed -i '/^#\?AllowAgentForwarding.*/d' /etc/ssh/sshd_config && \
42 echo "AllowAgentForwarding yes" >> /etc/ssh/sshd_config && \
43 \
44 sed -i '/^#\?GatewayPorts.*/d' /etc/ssh/sshd_config && \
45 echo "GatewayPorts no" >> /etc/ssh/sshd_config && \
46 \
47 sed -i '/^#\?UsePAM.*/d' /etc/ssh/sshd_config && \
48 echo "UsePAM no" >> /etc/ssh/sshd_config && \
49 echo "ChallengeResponseAuthentication no" >> /etc/ssh/sshd_config && \
50 \
51 echo "ClientAliveInterval 60" >> /etc/ssh/sshd_config && \
52 echo "ClientAliveCountMax 3" >> /etc/ssh/sshd_config && \
53 echo "LogLevel DEBUG3" >> /etc/ssh/sshd_config
54
55# Generate SSH host keys if they don't exist
56RUN ssh-keygen -A
57
58# Expose the SSH port
59EXPOSE 22
60
61# Copy the entrypoint script into the image
62COPY entrypoint.sh /usr/local/bin/entrypoint.sh
63RUN chmod +x /usr/local/bin/entrypoint.sh
64
65# Use tini to manage the entrypoint script. The script itself runs as root.
66ENTRYPOINT ["/sbin/tini", "--"]
67CMD ["/usr/local/bin/entrypoint.sh"]
Normally, I’m a fan of using tinyssh
for basic containers but since this is
meant to be a jump server, we need AllowTcpForwarding
and so openssh
is
simpler. Combined with this entrypoint.sh
:
1#!/usr/bin/env sh
2
3# Exit immediately if a command exits with a non-zero status
4set -e
5
6echo "Starting SSH daemon..."
7/usr/sbin/sshd -e
8
9echo "SSH daemon active. Starting OpenConnect interactively..."
10echo "You will be prompted for your VPN password."
11
12# Now, execute OpenConnect in the foreground.
13# This script runs as root by default in Docker, which OpenConnect needs
14# to modify network routes and create the tun interface.
15# The --script flag is vital for DNS/routing updates.
16exec openconnect sample.vpnhost \
17 -u me@sample.vpnhost \
18 --useragent='AnyConnect' \
19 --script=/etc/vpnc/vpnc-script
Here the command is hardcoded, but it can be adapted or parameterized via an environment variable if required.
Built with:
1# Remember to modify this for your key
2export PUB_KEY_CONTENT=$(cat ~/.ssh/cstuff.pub)
3# Build the container called vpn-smart-jumphost
4docker build \
5 --build-arg USER_PUBLIC_KEY="$PUB_KEY_CONTENT" \
6 --build-arg SSH_USER_NAME=jumphostuser \
7 --pull \
8 -t vpn-smart-jumphost:latest .
Which is subsequently run via:
1docker run --rm -it \
2 --name vpn-smart-jump-container \
3 --cap-add=NET_ADMIN \
4 --device=/dev/net/tun:/dev/net/tun \
5 -p 127.0.0.1:2200:22 \
6 vpn-smart-jumphost:latest
This will prompt in the terminal for the VPN credentials, including an MFA if
necessary. The network within the container is now correctly connected through
to the VPN. Remember to keep this open as it maintains the VPN session (perhaps
using tmux
).
ProxyJump
settings
For the SSH configuration now,
1Host vpn-docker-jump
2 HostName 127.0.0.1
3 Port 2200
4 User jumphostuser
5 IdentitiesOnly yes
6 IdentityFile ~/.ssh/cstuff
7
8Host *.vpnhost !vpnhost
9 User rgoswami
10 ProxyJump vpn-docker-jump
11
12Host someone.cstuff
13 Hostname something.vpnhost
14 User rgoswami
15 ProxyJump vpn-docker-jump
Replace ~/.ssh/cstuff
with the path to the private key that corresponds to the
public key you used during the build. Replace rgoswami
with your username on
the target systems if it’s different.
Usage
Once the container is running, the VPN is connected within it, and your
~/.ssh/config
is set up, you can SSH to your remote machines from a new
terminal window on your host:
1ssh someone.cstuff
Or indeed, anything which matches *.vpnhost
:
1ssh some-other-server.vpnhost
The first time you connect via the jumphost, you may be asked to verify and accept the SSH host key of the container.
Mounting SFTP Remotes
The ProxyJump
setup in ~/.ssh/config
is excellent for interactive SSH
sessions, scp
, and direct sftp
commands. However, for rclone
(homepage),
which can mount a remote SFTP share, some more work is necessary for the VPN
jumphost.
rclone
can leverage SSH’s capabilities, but it does not parse directives from
the global SSH config directly.
We can achieve this by constructing an appropriate ProxyCommand and passing it to rclone via its SFTP backend options.
1ssh -o "ProxyCommand ssh jumphostuser@127.0.0.1 -p 2200 -i ~/.ssh/cosmolab -o IdentitiesOnly=yes -W %h:%p" $USER@$VPN_HOSTNAME
The ProxyCommand effectively tells SSH how to connect to the intermediate jumphost first, and then tunnel the connection to the final SFTP server.
Essentially we have rclone config edit
(followed by n
and then SFTP
) to
end up with:
- type
sftp
- host
$RCLONE_REMOTE
- user
$USER
- pubkey_file
$SSH_KEY
- key_use_agent
true
- ssh
ssh -o "ProxyCommand ssh jumphostuser@127.0.0.1 -p 2200 -i ~/.ssh/cosmolab -o IdentitiesOnly=yes -W %h:%p" $USER@$VPN_HOSTNAME
Final usage proceeds in the usual fashion:
1rclone mount "$RCLONE_REMOTE:/home/$USER" ~/mnt/somevpn --vfs-cache-mode full
Where --vfs-cache-mode full
improves performance by caching files.
Conclusions
This containerized approach provides an isolated VPN connection and a convenient
ProxyJump
setup, allowing for more streamlined and targeted access to remote
networks without impacting your entire host’s networking. Remote file access via
rclone
also works well, which means integration with emacs
and other IDEs is
a snap.
A more robust variant of this basic concept developed into the vpn-proxyjump project.
Series info
Remote Usage series
- Simplified SSH access via container VPNs <-- You are here!