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

  1. Simplified SSH access via container VPNs <-- You are here!