Skip to content

Instantly share code, notes, and snippets.

@assarbad
Last active August 12, 2026 21:16
Show Gist options
  • Select an option

  • Save assarbad/d297ffa0fabb194eb405ac0f342c62ee to your computer and use it in GitHub Desktop.

Select an option

Save assarbad/d297ffa0fabb194eb405ac0f342c62ee to your computer and use it in GitHub Desktop.
Strictly hardened autossh systemd unit template for port-forwarding

How to "install" the enclosed unit template (all require superuser permissions)

  1. place the autossh@.service file in /etc/systemd/system
  2. enable an instance -- suppose your remote host is called frodo then you might run: systemctl enable autossh@frodo (do not use --now here!); IPv4 addresses also worked for me
  3. now it is time to create an override specific to the instance, e.g. : systemctl edit autossh@frodo.service and adjust the following bunch of settings (commented out in the main unit template) like so:
    [Service]
    Environment=REMOTE_PORT=1022
    Environment=TARGET_USER=username
    Environment=TARGET_HOST=frodo.example.com
    
    adjust the ports, username and host name as needed. The actual template unit contents are inside your editor, so you may override any of the other Environment= stanzas or provide a different LoadCredential= stanza!
    NB: you must not forget the [Service] to establish the section context.
    • This should have created a file /etc/systemd/system/autossh@frodo.service.d/override.conf
  4. once you're done run: systemctl daemon-reload for the changes to take effect
  5. create an SSH identity in /etc/ssh/id_nobody using ssh-keygen -f; alternatively adjust the LoadCredential= stanza to some other credential you want to use
    • if you opt for a different path it probably makes sense to also place the LoadCredential= stanza into the override.conf
  6. run systemd-analyze security autossh@frodo.service and witness something along the lines of:
    → Overall exposure level for autossh@frodo.service: 1.1 OK 🙂
    
  7. start the service with systemctl start autossh@frodo.service and observe any issues with journalctl -f (if you need more context, i.e. previous lines, throw in a -n 200 or so)

PS: to test the unit without installing, fake it as follows:

systemd-analyze security --offline=yes --instance=example.com autossh@.service
[Unit]
Description=Establish persistent SSH tunnel (%i)
Requires=ssh.service
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=0
[Service]
DynamicUser=true
# Import identity into unit namespace
LoadCredential=identity:/etc/ssh/id_nobody
# Following commented out lines exist for copy&pasta into the override.conf files
; LoadCredential=identity:/etc/ssh/id_nobody
; Environment=REMOTE_PORT=21022
; Environment=REMOTE_SSH_PORT=60022
; Environment=TARGET_USER=username
Environment=AUTOSSH_POLL=60
Environment=AUTOSSH_FIRST_POLL=30
Environment=AUTOSSH_LOGLEVEL=7
Environment=AUTOSSH_GATETIME=0
# Middle part must be in sync with LogsDirectory= ...
Environment=AUTOSSH_LOGFILE=%L/autossh/%i.log
LogsDirectory=autossh
Environment=MONITOR_PORT=0
Environment=TARGET_HOST=%i
Environment=LOCAL_SSH_PORT=22
ExecStart=/usr/bin/autossh -M ${MONITOR_PORT} -- -4Nngi %d/identity -F /dev/null -R localhost:${REMOTE_PORT}:localhost:${LOCAL_SSH_PORT} -o IdentitiesOnly=yes -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o PubkeyAuthentication=yes -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -o BatchMode=yes -o LogLevel=ERROR -p ${REMOTE_SSH_PORT} ${TARGET_USER}@${TARGET_HOST}
RestartSec=5
Restart=always
AmbientCapabilities=
CapabilityBoundingSet=
NoNewPrivileges=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=~cgroup
RestrictNamespaces=~ipc
RestrictNamespaces=~mnt
RestrictNamespaces=~net
RestrictNamespaces=~pid
RestrictNamespaces=~user
RestrictNamespaces=~uts
RestrictNamespaces=~time
RestrictRealtime=true
RestrictSUIDSGID=true
PrivateTmp=true
PrivateDevices=true
PrivateIPC=true
PrivateUsers=true
KeyringMode=private
InaccessiblePaths=-/usr/local/bin
SystemCallArchitectures=native
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@mount
SystemCallFilter=~@obsolete
SystemCallFilter=~@privileged
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@resources
SystemCallFilter=~@swap
LockPersonality=true
MemoryDenyWriteExecute=true
RemoveIPC=true
SystemCallErrorNumber=EPERM
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
ProtectSystem=strict
# following is not needed as long as we suppress reading /etc/ssh/ssh_config via -F /dev/null
# ReadOnlyPaths=/etc/ssh
UMask=0077
# ----------------------------------------------------------------------
# Residual items flagged by `systemd-analyze security` are intentional:
# systemd-analyze security --offline=yes --instance=<host> autossh@.service
#
# PrivateNetwork= (0.5) -- tunnel requires the host network stack.
# RestrictAddressFamilies=AF_INET -- TCP/IP egress to the remote host.
# /AF_INET6 -- same, for IPv6-capable remotes.
# RestrictAddressFamilies=AF_UNIX -- glibc NSS uses Unix sockets (nscd,
# nss-systemd/userdb) and journald.
# IPAddressDeny= (0.2) -- remote host is dynamic (%i), so no
# static IP allow-list is possible.
# RootDirectory=/RootImage= (0.1) -- host root is required; a chroot gains
# nothing and breaks NSS/credentials.
# DeviceAllow= char-rtc:r (0.1) -- auto-generated by ProtectClock=true,
# which mounts /dev/rtc read-only.
# ----------------------------------------------------------------------
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment