How to SSH to your Docker Container through a Chisel TCP Tunnel

Table of Content

It is considered a bad practice to connect to a Docker container through SSH, because everything should be done through CI/CD and you should analyze different metrics through a system monitoring, but if you are looking for how to SSH to a Docker container then you know what you do and you really need it. There are many reasons why you could want to do this, for example, for testing purposes, hotfixes, etc. Maybe you are configuring a new infrastructure for a new project and you want to check some nuances directly into a Docker container before to release the production.

While docker exec is the preferred approach, there are still scenarios where SSH might be useful. Many hosting services provide support for Docker but some of them don’t provide a direct access to your Docker container and it can be pain in the butt. That was my reason, so I started to find a solution how to connect to Docker if there is no direct access. After many researches I found a very basic answer on stackoverflow where the author describes how to achieve this with Chisel. There was no detailed example but there was the concept and I started to investigate it more deeply.

The problem

There is a remote Docker instance with a Bot box running on it. It’s behind the firewall and has no access from outside, but there is a requirement to connect to the Bot box through SSH\ periodically and check some metrics.

The solution

To simplify the configuration, the Docker instance with the Bot box will be spun up on my localhost. The main configuration will be wrapped with docker-compose and will be run smoothly with docker-compose up command. No additional requirements! When the configuration is ready and checked locally we can deploy it anywhere.

Setup the SSH Server

Installing the SSH Server in an Alpine Docker Container

Almost all popular Docker images are provided without a pre-installed SSH server. It has two reasons: safety and lightweight of an image. We are going to use the Alpine image of Docker and will need to add the OpenSSH server on our own. To be able to run the docker container continuously we will use Nginx demon. You know, if there is no background process running in a docker container then the container is stopped immediately? Instead of Nginx you can use any other package such as PHP, Python, etc.

Here’s an example of Dockerfile for Alpine-based image:

FROM alpine:latest

# Install required packages
RUN apk add --update bash openssh nginx

# Configure OpenSSH server
RUN sed -i -e 's/PermitRootLogin prohibit-password/PermitRootLogin yes/g' \
    -e 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config
RUN adduser -h /home/popov -s /bin/bash -D popov
RUN echo -n 'popov:mySuperSecretPassword' | chpasswd

# Create entrypoint.sh for SSH configuration
RUN printf '#!/usr/bin/env bash\n\nssh-keygen -A\n\nexec /usr/sbin/sshd -D -e "$@"\n' > /entrypoint.sh \
  && chmod +x -v /entrypoint.sh

# Run Nginx and OpenSSH in foreground
CMD (nginx -g 'daemon off;' &) && /entrypoint.sh

At the beginning we install required packages bash, openssh and nginx.

The next few lines allow root user to connect to the container (PermitRootLogin yes) and also allow use password to login into the container (PasswordAuthentication yes) (it’s considered to be less secure but let’s omit it in this example). Then a new user called popov with a home directory (-h) is created. The -s switch sets the user’s default login shell to Bash.

The echo -n 'popov:mySuperSecretPassword' | chpasswd changes the user’s password.

After this we create a script entrypoint.sh to run the SSH server.

The use of CMD ensures the Nginx server and the SSH service always start when the container does. Execution is then handed off to Bash as the container’s foreground process. You could replace this with your application’s binary.

Connecting to the Container

Now you’re ready to connect to your container. Build you container as follows:

docker build -t alpine-sshd .

Run the container with port 22 bound to the host’s 2002 port:

docker run --name sshd_app -p 2002:22 alpine-sshd:latest

Output:

ssh-keygen: generating new host keys: RSA DSA ECDSA ED25519
Server listening on 0.0.0.0 port 22.
Server listening on :: port 22.

Running ssh popov@<docker-container-ip> will give you a shell inside your container.

You can skip binding the port if you’ll be connecting from the machine that’s hosting the Docker container. Use docker inspect to get your container’s IP address, then pass it to the SSH connection command.

docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sshd_app
172.17.0.2

Use the SSH client on your machine to connect to the container:

ssh popov@172.17.0.2

Or you can use an forwarded port that we bounded to the host. Here’s how to initiate a connection when SSH is bound to port 2002:

ssh popov@localhost -p 2002

Here you should see a greeting message meaning the SSH server works smoothly.

Let’s clean it out because it’s only small part of the full configuration and it can interfere us in the next steps.

docker stop sshd_app
docker rm sshd_app

Setup the Reverse Tunnel Chisel Server

Now we’ll start up Chisel in server mode, since we want the Bot box to connect back to us. You need to prepare a : pair to protect this tunnel server. Replace them in the command below and prefer it in the client.

docker run -p 12398:12398 -p 2222:2222 --rm -d jpillora/chisel server --auth <USER_NAME>:<PASSWORD>  --port 12398 --reverse &

Here we forward two ports into the docker container:

  • -p 12398: our Chisel server is listening to on this port and our Chisel client will connect to this port in the next step. This port only allow to establish the tunnel between the Chisel server and the Chisel client.
  • -p 2222: we’ll connect to this port from any host and the Chisel server will forward it directly to the Chisel client. Actually, it’s our SSH port we’ll be able to reach the Bot box through.

Setup the Bot box client connection

On the Bot box, we’ll wrap everything up in a docker-compose configuration. The bot service uses Dockerfile that we created above. The following configuration will instruct the Chisel client to connect back to the Chisel server on port 12398. Once connected, we’ll forward any traffic sent to <SERVER_IP>:2222 to port 22 on the Bot box.

version: '2'
services:
    bot:
        build: .
        ports:
            - "2002:22"
        volumes:
            - .:/app
    chisel:
        # ⬇️ Delay starting Chisel server until the web server container is started.
        image: jpillora/chisel
        restart: unless-stopped
        container_name: 'chisel'
        command:
          - 'client'
          # ⬇️ Use username `<USER_NAME>` and password `<PASSWORD>` to authenticate with Chisel server.
          - '--auth=<USER_NAME>:<PASSWORD>'
          # ⬇️ Domain & port of Chisel server. Port defaults to 12398 on server, but must be manually set on client.
          - '<SERVER_IP>:12398'
          # ⬇️ Reverse tunnel traffic from the Chisel server to the Bot box container, identified in Docker using DNS by its service name `bot`.
          - 'R:2222:bot:22'
        ports: 
          - "2222:22"

The most intriguing moment. Drum roll. Run command:

docker-compose up

Output:

Creating network "chisel_bot_default" with the default driver
Creating chisel              ... done
Creating chisel_bot_1 ... done
Attaching to chisel, chisel_bot_1
chisel    | 2022/04/06 02:31:34 client: Connecting to ws://<SERVER_IP>:12398
web_1     | ssh-keygen: generating new host keys: RSA DSA ECDSA ED25519 
web_1     | Server listening on 0.0.0.0 port 22.
web_1     | Server listening on :: port 22.

Now we have to be sure that we’re able to connect to port 22 of the Bot box, through the tunnel, from the host (your localhost).
There is a few steps to check if we can establish SSH connection with our Bot box.
Open a new terminal.

Note: Following steps only work if you run your Chisel client on the host. If your Chisel client is on a remote host, obviously it won’t work and you can go directly to the Step 3.

Retrieve IP of our Bot container:

docker ps
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <DOCKER_CONTAINER_NAME>

Output:

192.168.128.3

Step 1

Check if we can connect to the Bot box directly to 22.

Try to connect to the Bot container:

ssh popov@192.168.128.3

As you can notice here we don’t use any port that means we use default 22 port and we omit any Docker or Chisel port forwarding.

Step 2

Check if Docker port forwarding works correctly. The port 22 of the Bot box is forwarded through Docker to 2002 port of the host. Here we use only Docker configuration and omit Chisel.

ssh popov@localhost -p 2002

Step 3

The most important step, such as it should check if the connection is established despite where your Bot box is situated. Here we use Chisel configuration together with Docker port forwarding. We connect to 2222 port of our Chisel server that have the tunnel established with our Chisel client that have access to the Bot box through the internal network of Docker.

ssh -popov@<SERVER_IP> -p 2222

Useful links

Leave a Reply

Your email address will not be published. Required fields are marked *