Skip to content

Instantly share code, notes, and snippets.

@olbat
Last active May 14, 2024 15:42
Show Gist options
  • Save olbat/0e4879825b01239d8d6064d239e0723b to your computer and use it in GitHub Desktop.
Save olbat/0e4879825b01239d8d6064d239e0723b to your computer and use it in GitHub Desktop.
systemd SSH tunnel

Create SSH tunnels using systemd units

Table of contents

Overview

This systemd service scripts will allow you to create SSH tunnels in background using systemd in user mode and a configuration file.

Setup the SSH agent systemd unit

To be able to run SSH tunnels in background while using a password protected SSH key to connect to the targeted host, we'll first have to setup an ssh-agent (see ssh-agent(1)).

The agent will be run in background using a user systemd service unit (see systemd.service(5), systemd.unit(5), Arch Linux wiki page).

# (create the systemd config directory if it does not exists)
mkdir -p ~/.config/systemd/user/

# create the ssh-agent.service unit
cat <<'EOF' > ~/.config/systemd/user/ssh-agent.service
[Unit]
Description=SSH key agent
Documentation=man:ssh-agent(1)
Wants=environment.target
Before=environment.target

[Service]
Type=forking
Environment="SSH_AUTH_SOCK=%t/ssh-agent.socket"
ExecStart=/usr/bin/ssh-agent -a $SSH_AUTH_SOCK
ExecStartPost=/bin/systemctl --user set-environment \
  SSH_AUTH_SOCK=${SSH_AUTH_SOCK}

[Install]
WantedBy=multi-user.target
EOF

Once it's done we can start the authentication agent:

systemctl --user start ssh-agent

Or even make it load at session opening:

systemctl --user enable ssh-agent

After that, we make the $SSH_AUTH_SOCK env variable be loaded at our shell initialization (here bash):

echo 'export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"' >> ~/.bashrc
source ~/.bashrc

Finally we add our (default) private key identity to the authentication agent using ssh-add (see ssh-add(1)).

ssh-add

Note: it has to be done at every boot in order for the tunnels to work !

Setup the SSH-tunnel systemd unit

As for the SSH authentication agent service, we'll run our SSH tunnels (ssh(1)) using a systemd service unit but this unit will be instanciable (see systemd.unit(5), Lennart Poettering's blog, Arch Linux wiki page, Fedoramagazine article).

# (create the systemd config directory if it does not exists)
mkdir -p ~/.config/systemd/user/

# create the [email protected] unit
cat <<'EOF' > ~/.config/systemd/user/[email protected]
[Unit]
Description=SSH tunnel to %i
Documentation=man:ssh(1)
Wants=ssh-agent.service
After=network.target ssh-agent.service

[Service]
Type=simple
Environment="LOCAL_ADDR=127.0.0.1"
Environment="REMOTE_HOST=127.0.0.1"
EnvironmentFile=%h/.ssh/tunnels/%i
ExecStart=/usr/bin/ssh -NT ${TARGET} \
  -o ExitOnForwardFailure=yes -o ServerAliveInterval=60 \
  -L ${LOCAL_ADDR}:${LOCAL_PORT}:${REMOTE_HOST}:${REMOTE_PORT} ${SSH_OPTS}
RestartSec=10
Restart=on-success
RestartForceExitStatus=255

[Install]
WantedBy=multi-user.target
EOF

Create tunnels using systemd services

We'll now create systemd service instances to run our tunnels.

Our ssh-tunnel service is going to look for it's configuration file into the ~/.ssh/tunnels/ directory so we start with creating the directory:

mkdir -p ~/.ssh/tunnels/

We'll then have to create a configuration file for our tunnel:

cat <<'EOF' > ~/.ssh/tunnels/testtun
# This parameters will be used to run:
#     ssh ${TARGET} ${SSH_OPTS} \
#     -L ${LOCAL_ADDR}:${LOCAL_PORT}:${REMOTE_ADDR}:${REMOTE_PORT}

# The target of the SSH command
TARGET=user@hostname

# The local port to listen to
LOCAL_PORT=8080

# The remote port to forward
REMOTE_PORT=80

# The local address to listen to (default: _127.0.0.1_)
#LOCAL_ADDR=0.0.0.0

# The remote host to forward to (default: _127.0.0.1_)
#REMOTE_HOST=domain.tld

# Additional -custom- SSH options
#SSH_OPTS=-v
EOF

Finally we can run our systemd service unit that will create the tunnel:

systemctl --user start ssh-tunnel@testtun

Or make it load a session loading:

systemctl --user enable ssh-tunnel@testtun

Note: make sure your SSH authentication agent has been loaded properly using ssh-add, the tunnel will not work without it

We can check the tunnel's status using:

systemctl --user status -l ssh-tunnel@testtun

Or list the systemd services of current user:

systemctl --user list-units --type=service

Examples

Create a tunnel to a web server on a machine that's only accessible by SSH

Let's create a configuration file to access to the web server (80 port) of the www.privatecloud.domain.tld machine and make the tunnel accessible to other machines over the network:

cat <<'EOF' > ~/.ssh/tunnels/privatecloud-www
[email protected]
LOCAL_ADDR=0.0.0.0
LOCAL_PORT=8080
REMOTE_PORT=80
EOF

Then start the tunnel and make it load at session loading:

systemctl --user start ssh-tunnel@privatecloud-www
systemctl --user enable ssh-tunnel@privatecloud-www

We can now connect to the web server using:

curl http://127.0.0.1:8080/

or (from another machine that's in the same network):

curl http://mymachine.domain.lan:8080/

Create a tunnel to a MySQL service only accessible through an SSH bastion (directly)

Let's create a configuration file to access to the MySQL server (3306 port) of the mysql.privatecloud.domain.tld machine that's only through the access.privatecloud.domain.tld bastion:

cat <<'EOF' > ~/.ssh/tunnels/privatecloud-mysql
[email protected]
LOCAL_PORT=3306
REMOTE_PORT=3306
REMOTE_HOST=mysql.privatecloud.domain.tld
EOF

Then start the tunnel:

systemctl --user start ssh-tunnel@privatecloud-mysql

We can now connect to the mysql.privatecloud's MySQL service using:

mysql -h 127.0.0.1

Create a tunnel to a MySQL service only accessible through an SSH bastion (using SSH)

In this example, we want to create the same tunnel than the one in the previous example but this time the MySQL service is not accessible directly from the bastion (on port 3306).

Fortunately, the mysql.privatecloud machine is accessible from the bastion using SSH so we can create a tunnel using an ssh jump (see ssh(1), wikibooks article):

cat <<'EOF' > ~/.ssh/tunnels/privatecloud-mysql
TARGET=mysql.privatecloud.domain.tld
LOCAL_PORT=3306
REMOTE_PORT=3306
REMOTE_HOST=127.0.0.1
SSH_OPTS="-J access.privatecloud.domain.tld:22"
EOF

Then start the tunnel:

systemctl --user start ssh-tunnel@privatecloud-mysql

We can now connect to the mysql.privatecloud.domain.tld's MySQL service using:

mysql -h 127.0.0.1

Further reading

@ankostis
Copy link

ankostis commented Feb 18, 2021

You may want to look to on-demand aocket-activation for ssh tunnels on stackexchange, that supports stop-on-idle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment