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 |