This is the sixteenth part of the Availability Anywhere series. For your convenience you can find other parts in the table of contents in Part 1 – Connecting to SSH tunnel automatically in Windows
Let’s say that we want to expose a port from the host machine to the docker container. For instance, we have a locally installed database and we want to access it from the docker container. If we’re on Linux, then we can use --network host
and that will do the trick for us. However, this option is not supported on Mac or Windows. Let’s see how to do it in that case.
We are going to run an OpenSSH server inside the docker network, connect to it from the host, forward a remote port, and then connect to localhost.
Let’s start with creating a directory for the docker project:
1 |
mkdir -p ssh_tunnel |
Let’s create SSH keys.
1 |
ssh-keygen -b 2048 -t rsa -f ssh_tunnel/tunnel_rsa |
Next, create the configuration for OpenSSH. Just a default configuration with port forwarding enabled (AllowTcpForwarding set to yes). This is in file ssh_tunnel/sshd_config
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ # This is the sshd server system-wide configuration file. See # sshd_config(5) for more information. # This sshd was compiled with PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # The strategy used for options in the default sshd_config shipped with # OpenSSH is to specify options with their default value where # possible, but leave them commented. Uncommented options override the # default value. Port 2222 #AddressFamily any #ListenAddress 0.0.0.0 #ListenAddress :: #HostKey /etc/ssh/ssh_host_rsa_key #HostKey /etc/ssh/ssh_host_ecdsa_key #HostKey /etc/ssh/ssh_host_ed25519_key # Ciphers and keying #RekeyLimit default none # Logging #SyslogFacility AUTH #LogLevel INFO # Authentication: #LoginGraceTime 2m #PermitRootLogin prohibit-password #StrictModes yes #MaxAuthTries 6 #MaxSessions 10 #PubkeyAuthentication yes # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 # but this is overridden so installations will only check .ssh/authorized_keys AuthorizedKeysFile .ssh/authorized_keys #AuthorizedPrincipalsFile none #AuthorizedKeysCommand none #AuthorizedKeysCommandUser nobody # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts #HostbasedAuthentication no # Change to yes if you don't trust ~/.ssh/known_hosts for # HostbasedAuthentication #IgnoreUserKnownHosts no # Don't read the user's ~/.rhosts and ~/.shosts files #IgnoreRhosts yes # To disable tunneled clear text passwords, change to no here! PasswordAuthentication no #PermitEmptyPasswords no # Change to no to disable s/key passwords #KbdInteractiveAuthentication yes # Kerberos options #KerberosAuthentication no #KerberosOrLocalPasswd yes #KerberosTicketCleanup yes #KerberosGetAFSToken no # GSSAPI options #GSSAPIAuthentication no #GSSAPICleanupCredentials yes # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will # be allowed through the KbdInteractiveAuthentication and # PasswordAuthentication. Depending on your PAM configuration, # PAM authentication via KbdInteractiveAuthentication may bypass # the setting of "PermitRootLogin without-password". # If you just want the PAM account and session checks to run without # PAM authentication, then enable this but set PasswordAuthentication # and KbdInteractiveAuthentication to 'no'. #UsePAM no #AllowAgentForwarding yes # Feel free to re-enable these if your use case requires them. AllowTcpForwarding yes GatewayPorts no X11Forwarding no #X11DisplayOffset 10 #X11UseLocalhost yes #PermitTTY yes #PrintMotd yes #PrintLastLog yes #TCPKeepAlive yes #PermitUserEnvironment no #Compression delayed #ClientAliveInterval 0 #ClientAliveCountMax 3 #UseDNS no PidFile /config/sshd.pid #MaxStartups 10:30:100 #PermitTunnel no #ChrootDirectory none #VersionAddendum none # no default banner path #Banner none # override default of no subsystems Subsystem sftp internal-sftp # Example of overriding settings on a per-user basis #Match User anoncvs # X11Forwarding no # AllowTcpForwarding no # PermitTTY no # ForceCommand cvs server |
And the ssh_tunnel/Dockerfile
:
1 2 3 |
FROM lscr.io/linuxserver/openssh-server:latest COPY sshd_config /config/ssh_host_keys/sshd_config |
We could mount the volume instead, but I couldn’t get it to work with file permissions in Mac. Copying the file is good enough.
That’s it. We can now start the machinery. First, build the image:
1 |
docker build -t image_ssh ssh_tunnel |
Now, let’s start or run the container with OpenSSH server:
1 2 3 4 5 6 7 8 9 |
docker start container_ssh 2>/dev/null || docker run -d \ --name=container_ssh \ -e TZ=Etc/UTC \ -e "PUBLIC_KEY=YOUR_GENERATED_PUBLIC_KEY_HERE" \ -p 127.0.0.1:58222:2222 \ -p 127.0.0.1:YOUR_APPLICATION_PORT:YOUR_APPLICATION_PORT \ -e USER_NAME=tunnel \ --restart unless-stopped \ image_ssh |
Now, let’s clear the saved SSH fingerprint (in case when we restarted everything), kill existing session (if there is one), and connect to the container:
1 2 3 |
ssh-keygen -R '[localhost]:58222' pkill -f "ssh -i $(pwd)/ssh_tunnel/tunnel_rsa tunnel@localhost -p 58222" ssh -i "$(pwd)"/ssh_tunnel/tunnel_rsa tunnel@localhost -p 58222 -4 -o StrictHostKeyChecking=no -R YOUR_DEPENDENCY_PORT:127.0.0.1:YOUR_DEPENDENCY_PORT -fN |
YOUR_DEPENDENCY_PORT
is the port you’d like to forward to the container. This could be 5432
of your PostgreSQL server hosted locally, for instance. You can also see that we expose two ports from the container: 2222
as 58222
in order to connect to the OpenSSH server, and YOUR_APPLICATION_PORT
which can be another application or dependency that you’d like to expose to another docker container or to external world.
Let’s now start or run our application:
1 2 3 4 |
docker start container_application 2>/dev/null || docker run \ --name=container_application \ --network 'container:container_ssh' \ image_application |
This way the application starts in the same network, so it can access localhost:YOUR_DEPENDENCYPORT
that will go to the host via OpenSSH. At the same time, your container exposes YOUR_APPLICATION_PORT
the regular way, so you can then configure additional tunnels.
You can clean all things up with the following (mind that it may remove some of your unused things):
1 2 3 4 5 6 7 8 9 10 |
ssh-keygen -R '[localhost]:58222' pkill -f "ssh -i $(pwd)/ssh_tunnel/tunnel_rsa tunnel@localhost -p 58222" docker stop container_application docker rm container_application docker rmi --force image_application docker stop container_ssh docker rm container_ssh docker rmi image_ssh docker system prune docker volume prune |
Tested with Amazon Linux 2 and Ventura Mac with M1 chip. Should work with Windows as well, as long as you install OpenSSH client on your host (or use putty instead) and have some decent terminal. If you prefer to use PowerShell, then go with this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
$location = (get-location).path $Key = "${location}\ssh_tunnel\tunnel_rsa" Icacls $Key /c /t /Inheritance:d TakeOwn /F $Key Icacls $Key /c /t /Grant:r ${env:UserName}:F Icacls $Key /c /t /Remove:g Administrator "Authenticated Users" BUILTIN\Administrators BUILTIN Everyone System Users Icacls $Key docker build -t image_ssh ssh_tunnel docker start container_ssh if($LASTEXITCODE -ne 0){ docker run -d ` --name=container_ssh` -e TZ=Etc/UTC ` -e "PUBLIC_KEY=YOUR_GENERATED_PUBLIC_KEY_HERE" ` -p 127.0.0.1:58222:2222 ` -p 127.0.0.1:YOUR_APPLICATION_PORT:YOUR_APPLICATION_PORT ` -e USER_NAME=tunnel ` --restart unless-stopped ` image_ssh } sleep 5 ssh-keygen -R '[localhost]:58222' $replacedLocation = $location.replace("\", "/") (Get-WmiObject win32_process -filter "Name='ssh.exe' AND CommandLine LIKE '%${replacedLocation}/ssh_tunnel/tunnel_rsa tunnel@localhost -p 58222%'").Terminate() $sshStartBlock = { param([string]$pwd) ssh -i "$pwd/ssh_tunnel/tunnel_rsa" tunnel@localhost -p 58222 -4 -o StrictHostKeyChecking=no -R YOUR_DEPENDENCY_PORT:127.0.0.1:YOUR_DEPENDENCY_PORT -fN } start-job -ScriptBlock $sshStartBlock -ArgumentList $replacedLocation docker start container_application if($LASTEXITCODE -ne 0){ docker run ` --name=container_application ` --network 'container:container_ssh' ` image_application } |
This was tested with English-based Windows Server 2016 Datacenter. You may need to replace user names for different locales. Also, connecting to localhost
should work with IPv4. If it defaults to IPv6, then change it to 127.0.0.1
Cleaning in PowerShell:
1 2 3 4 5 6 7 8 9 10 11 12 |
ssh-keygen -R '[localhost]:58222' $location = (get-location).path $replacedLocation = $location.replace("\", "/") (Get-WmiObject win32_process -filter "Name='ssh.exe' AND CommandLine LIKE '%${replacedLocation}/ssh_tunnel/tunnel_rsa tunnel@localhost -p 58222%'").Terminate() docker stop container_application docker rm container_application docker rmi --force image_application docker stop container_ssh docker rm container_ssh docker rmi image_ssh docker system prune docker volume prune |
What’s more, we can make this dance with keys even simpler by using sish. That’s basically an OpenSSH server that allows for empty password authentication and other stuff, just like serveo.net