How to Manage Multiple SSH Tunnels with a Single Systemd Service File


2 views

When maintaining multiple SSH tunnels, creating individual service files for each connection becomes tedious. The standard approach leads to code duplication where only the remote host parameter differs between files:

[Unit]
Description=SSH Tunnel to server1
After=network.target

[Service]
ExecStart=/usr/bin/autossh -N server1.example.com -L 8080:localhost:80
Restart=always

[Install]
WantedBy=multi-user.target

Systemd provides parameterized unit files that solve this elegantly. Create a template file named ssh-tunnel@.service:

[Unit]
Description=SSH Tunnel to %I
After=network.target

[Service]
ExecStart=/usr/bin/autossh -N %i -L 8080:localhost:80
Restart=always

[Install]
WantedBy=multi-user.target

Now instantiate multiple instances with:

systemctl enable ssh-tunnel@server1.service
systemctl enable ssh-tunnel@server2.service

For more complex scenarios, combine template units with environment files:

# /etc/default/ssh-tunnels
SERVER1_ARGS="-L 8080:localhost:80 -L 8443:localhost:443"
SERVER2_ARGS="-L 3306:localhost:3306"

[Service]
EnvironmentFile=/etc/default/ssh-tunnels
ExecStart=/usr/bin/autossh -N %i $%i_ARGS

While systemd templates work well, infrastructure-as-code tools like Ansible provide better maintainability:

# ansible playbook snippet
- name: Configure SSH tunnels
  template:
    src: ssh-tunnel@.service.j2
    dest: /etc/systemd/system/ssh-tunnel@{{ item.host }}.service
  loop: "{{ ssh_tunnels }}"
  notify: reload systemd
  • Template units maintain atomicity - each instance can be controlled individually
  • Using %i for the instance name provides maximum flexibility
  • Combine with EnvironmentFile when port mappings differ between hosts
  • For large deployments, consider generating units via configuration management

Many sysadmins face the same issue: needing to maintain multiple SSH tunnels with nearly identical configurations. Creating separate .service files for each tunnel leads to maintenance headaches and violates the DRY principle.

While my initial thought was to request templating in systemd itself, I've since realized better solutions exist. Here's how we can handle this properly:

[Unit]
Description=SSH Tunnel to %I
After=network.target

[Service]
User=autossh
ExecStart=/usr/bin/autossh -M 0 -N -q -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" -p 22 -l autossh %i -L 7474:127.0.0.1:7474 -i /home/autossh/.ssh/id_rsa
Restart=always

[Install]
WantedBy=multi-user.target

Save this as /etc/systemd/system/ssh-tunnel@.service, then enable multiple instances:

systemctl enable ssh-tunnel@server1.example.com
systemctl enable ssh-tunnel@server2.example.com
systemctl enable ssh-tunnel@server3.example.com

While systemd templates work, I now prefer configuration management tools. Here's an Ansible example:

- name: Create SSH tunnel services
  template:
    src: ssh-tunnel@.service.j2
    dest: /etc/systemd/system/ssh-tunnel@{{ item.host }}.service
  loop: "{{ ssh_tunnels }}"
  vars:
    ssh_tunnels:
      - host: server1.example.com
        port: 7474
      - host: server2.example.com
        port: 7475
  • Use separate SSH keys per tunnel
  • Implement proper logging with -E flag
  • Consider TCP keepalive settings
  • Monitor tunnel health with external tools
Approach When to Use
systemd templates Small number of static tunnels
Configuration mgmt Dynamic environments, many tunnels
Containerized Isolated environments, cloud deployments