Docker: How to solve the trouble of SSH tunnel connection?

I wanted to connect to some external site via my server in AWS because the site has IP address filters as security. However my apartment doesn't have a static IP address so I came up with an idea connecting to the site via SSH tunnel and if it runs as a container in docker-compose, it’s very easy to execute by the command “docker-compose up -d.” But there was a pitfall when digging an SSH tunnel on docker. Let me share what the trap was and how to solve it.

The command which I executed on the CLI was like this.

1ssh -4f -NL 20000:hogeadmin.work:443 takashi@myadmin.server -p 10023

So, I made a simple container with the following Dockerfile to execute ssh client.
[ssh-tunnel/Dockerfile]

 1FROM alpine:latest
 2
 3RUN set -x \
 4    && apk update \
 5    && apk upgrade \
 6    && apk add --no-cache \
 7            openssh-client \
 8            ca-certificates \
 9            bash \
10            bind-tools
11
12EXPOSE 20000-20010
13
14ENTRYPOINT [ "/docker-entrypoint.sh" ]

And, I made a simple script as docker-entrypoint.sh.
[ssh-tunnel/docker-entrypoint.sh]

 1#!/bin/bash
 2REMOTE_HOST='x.x.x.x'
 3REMOTE_USER='user-name'
 4REMOTE_PORT='10023'
 5
 6connect_ssh_tunnel(){
 7    local local_port=$1; shift
 8    local remote_port=$1; shift
 9    local fqdn=$1; shift
10
11    ssh -4f -p ${REMOTE_PORT} -NL ${local_port}:${fqdn}:${remote_port} ${REMOTE_USER}@${REMOTE_HOST}
12}
13
14connect_ssh_tunnel '20000' '443' hogeadmin.work'
15
16# To keep running
17tail -f /dev/null

And then, I made a docker-compose.yml file like this.

 1version: '3.4'
 2services:
 3  ssh-tunnel:
 4    build:
 5      context: ./docker/ssh-tunnel
 6    restart: always
 7    container_name: ssh-tunnel
 8    ports:
 9      - 443:20000
10    volumes:
11      - ./docker/ssh-tunnel/docker-entrypoint.sh:/docker-entrypoint.sh:ro
12      - ~/.ssh:/root/.ssh:ro

So, I run the command “docker-compose up -d” to run the container. And then, I tried to connect via curl command with the “--resolve” option.

1$ curl --resolve hogeadmin.work:443:127.0.0.1 https://hogeadmin.work/
2curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to hogeadmin.work 

I was confused because it’s just a simple thing but I got the above error. So, I tried to connect via the telnet command to check the port is alive or not.

1$ telnet localhost 443
2Trying ::1...
3Connected to localhost.
4Escape character is '^]'.
5Connection closed by foreign host.

The connection was closed quickly. So, I tried to do the same thing in the container.

 1$ docker exec -it ssh-tunnel bash
 2
 3bash-5.1# apk update && apk add -f curl
 4fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
 5fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
 6v3.14.3-58-g7fc21b9dfb [https://dl-cdn.alpinelinux.org/alpine/v3.14/main]
 7v3.14.3-57-g005638434d [https://dl-cdn.alpinelinux.org/alpine/v3.14/community]
 8OK: 14942 distinct packages available
 9OK: 21 MiB in 40 packages
10
11bash-5.1# curl localhost:20000
12<html>
13<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
14<body>
15<center><h1>400 Bad Request</h1></center>
16<center>The plain HTTP request was sent to HTTPS port</center>
17</body>
18</html>

I got the HTTP response as the above. So, the SSH tunnel in itself works correctly but it’s not able to connect via the mapped port which is defined in the docker-compose.yml file. So, I searched about the ssh client command and I realized that the “-L” option has the optional parameter called “bind_address.” So, I add "0.0.0.0" as bind address to the command in the script like this.

1connect_ssh_tunnel(){
2   local local_port=$1; shift
3   local remote_port=$1; shift
4   local fqdn=$1; shift
5 
6   ssh -4f -p ${REMOTE_PORT} -NL 0.0.0.0:${local_port}:${fqdn}:${remote_port} ${REMOTE_USER}@${REMOTE_HOST}
7}

And then, I restarted the docker-compose and tried again.

 1$ curl --resolve hogeadmin.work:443:127.0.0.1 https://hogeadmin.work/
 2<!DOCTYPE HTML>
 3<html lang="ja">
 4    <head>
 5        <meta charset="utf-8">
 6        </meta>
 7    </head>
 8    <body>
 9        Forbidden
10    </body>
11</html>

Finally, I got the HTTP response so it works as I expected. However this way is simple way so it has some problems as follows.

  • It’s impossible to get access to some multiple sites at the same time
  • It will be a problem when the 443/tcp port is already used in other purpose

But I have a solution that is a little bit complicated so I’ll share the way in another article in this blog in the near future.