Making tailscaled dependable for sshd and other services
If you use Tailscale on your server, you may have services that should only listen on that IP. Unfortunately, the tailscaled service often goes active before it’s actually done, breaking dependencies: here’s how to fix it.
There are multiple Github issues pointing out that the tailscaled
systemd service does not behave very well and often says that it’s ready
before a bindable IP is actually available:
#11504 and
#3340 are two
examples.
In March 2024, almost a year ago as of this writing, it was pointed out that this looks like a regression, but since it still doesn’t seem to be fixed, it requires users to work around it.
One great way mentioned in #11504 is that you can add an ExecStartPost
attribute for the tailscaled service that ensures it’s actually done
before marking the service as running.
In this example, we’ll illustrate how to set up and make a dependency in the ssh daemon service more reliable, for situations where your tailscale ip is the only ip that you expect to use for ssh.
Step 1: Improve tailscaled.service
reliability with an override#
Run the following to open your editor, creating an override for the service object:
systemctl edit tailscaled
Read the comments on screen, and add the following between the two comment blocks as instructed:
[Service]
ExecStartPost=timeout 60s bash -c 'until tailscale status --peers=false; do sleep 1; done'
The comments will show you the current configuration of the service
object, where you may note that ExecStartPost
is not set. The way
overrides work is that anything specified will “merge” into the existing
service configuration without having to change it, which is good since
it’s often owned by the package itself. It follows the logic of
.d
-directories, which is also clear by the path chosen for these
override files.
When you have saved and exited from your editor, you can run the following to see the full “state” of your current service object configuration:
systemctl cat tailscaled
Note that your override shows up at the bottom.
Step 2: Configure sshd and create a dependency#
If you want your ssh daemon to only listen on a specific ip, the easiest
way to achieve this is to simply configure it in sshd_config
. Look for
the ListenAddress
attribute, and specify it once or several times,
depending on your use case. In this example, we are using both tailscale
and wireguard, so the sshd configuration looks like this:
ListenAddress 100.74.237.36
ListenAddress 10.10.10.5
Confirming that this configuration is active, after a restart if changed
recently, can be done by simply looking for the Port
configured in
sshd in the output of ss
:
ss -tuln | grep ':22'
tcp LISTEN 0 128 100.74.237.36:22 0.0.0.0:*
tcp LISTEN 0 128 10.10.10.5:22 0.0.0.0:*
If your sshd configuration is set up correctly, all you have to do is
create the service object dependency, to ensure that the ssh daemon
waits for tailscaled.service
before starting, which should now be a
bit more reliable with the above workaround.
Again, simply do:
systemctl edit ssh
A short tangent on lists in systemd: some attributes, like After
in
this case, are lists. You can tell that something is a list from the
way that it’s clearly a list:
Attribute=lists have multiple objects
Although it’s not a valid syntax, the above can be mentally parsed as:
Attribute=[lists, have, multiple, objects]
There are other attributes which are clearly not lists, like
Description
which takes a string, and only one string.
Because overrides will “merge” values, the default behavior when
overriding a list is to append to that list. If you want to actually
override the list, you therefore need to remember to null the list
first before overriding it. For example, if the current value is
Attribute=foo fax
and you want to completely override it, this
requires you to do:
[Section]
Attribute=
Attribute=bar baz
If you don’t do this, the end result of the override would be
Attribute=foo fax bar bax
.
This is good to know, but not strictly relevant in our case, as we
actually do want to just append new dependencies, so after running
systemctl edit ssh
we can simply add:
[Unit]
After=wg-quick@wg0.service tailscaled.service
If you are not using wireguard, this would obviously just be
tailscaled.service
.
After saving and running systemctl daemon-reload
, we can again verify:
systemctl cat ssh | grep After
After=network.target auditd.service
After=wg-quick@wg0.service tailscaled.service
If you want to be extra sure, you can also check the output of:
systemctl show ssh | grep After=
Now that both tailscaled.service
and ssh.service
are fixed up, you
can at your convenience try rebooting the server, first ensuring you have a
way of getting back in if it breaks, of course.
You can then, hopefully, observe the services starting in the correct order:
journalctl --boot | grep -E "ssh.service|tailscaled.service"
systemd[1]: Starting tailscaled.service - Tailscale node agent...
systemd[1]: Started tailscaled.service - Tailscale node agent.
systemd[1]: Starting ssh.service - OpenBSD Secure Shell server...
systemd[1]: Started ssh.service - OpenBSD Secure Shell server.