Skip to content
Français
Conteneurs & Orchestration medium

Incus OS: connecting nodes to a Tailscale tailnet

20 min de lecture

Read this page in French

incus logo

Administering an Incus OS cluster means reaching its nodes over the network. Rather than exposing the API or SSH (absent here) on a public network, you attach each node to a Tailscale tailnet: an encrypted mesh network where machines reach each other through a stable private 100.x address, wherever they sit. This guide shows how to enable the Incus OS Tailscale service entirely through the API, with no shell, verify the mesh is up, and advertise routes towards the instances. Intended audience: administrators of an Incus OS cluster.

What you will learn

  • Understand what the Tailscale service of Incus OS connects, and what it does not.
  • Generate an authentication key suited to a fleet of nodes.
  • Enable and configure the service through the API, without exposing the key.
  • Check the state of the mesh (100.x addresses, peers online).
  • Advertise routes, or point at a Headscale control server.

Prerequisites

  • A running Incus OS cluster. See Incus OS without a shell and Incus cluster.
  • A Tailscale account, or a Headscale control server publicly reachable over HTTPS, covered at the end of this guide.
  • An Incus client whose certificate is trusted, with a remote for each node.
  • Outbound internet access from the nodes, to reach the Tailscale control plane.

As with A/B updates, all configuration goes through the REST API prefixed with /os, queried with incus query. The Tailscale service is configured per node: every command targets one member through its remote.

The Incus OS Tailscale service

Unlike the Ceph client, the Tailscale client is already embedded in the Incus OS image: there is no add-on to install. The service is simply disabled by default. You enable it by writing its configuration, and Incus OS starts the tailscaled daemon and performs the attachment to the network.

One essential point before starting: the service connects the node (the Incus OS host) to the tailnet, not the instances it hosts. A container or a virtual machine does not get a Tailscale address just because its host is connected. To make instances reachable, you go through advertised routes (a subnet router), covered further down.

Generating an authentication key

Attaching a node non-interactively requires an auth key. From the Tailscale console, in the key settings, generate a key with the options that suit a fleet:

  • Reusable: a single key serves every node of the cluster.
  • Ephemeral: a disconnected node is removed automatically from the tailnet, which avoids ghost entries after a test or a decommission.
  • Short expiry: limit the window during which the key is valid.

The key looks like tskey-auth-.... It is a secret: it grants the right to enrol machines into your network. Do not store it in the clear, and do not leave it sitting in a shell history.

Enabling the service on one node

The configuration is written with a PUT on services/tailscale. To avoid exposing the key, read it with masked input, write a temporary file with tight permissions, then delete it. Start with a single node so you validate before generalising:

Fenêtre de terminal
umask 077
read -rsp 'Tailscale auth key: ' TSKEY; echo
cat > /tmp/ts.json <<JSON
{"config":{"enabled":true,"auth_key":"$TSKEY","accept_routes":false,"accept_dns":false,"login_server":""}}
JSON
unset TSKEY
incus query -X PUT "node1:/os/1.0/services/tailscale" -d "$(cat /tmp/ts.json)"

An empty login_server means the default Tailscale control plane, served over HTTPS with a valid public certificate. In response, Incus OS starts tailscaled and runs the equivalent of a tailscale up with the supplied key. The accept_routes field at false means the node does not import routes advertised by other peers; accept_dns at false leaves the node's local DNS resolution untouched.

Checking the connection

Attachment takes a few seconds. Then query the service state. Careful: the read also returns config.auth_key in the clear, so filter on the state part before sharing any output.

Fenêtre de terminal
incus query "node1:/os/1.0/services/tailscale"

The state section should show an active backend and a Tailscale address:

{
"state": {
"backend_state": "Running",
"version": "1.100.0",
"tailnet_name": "your-account",
"self": {
"host_name": "node1",
"dns_name": "node1.your-tailnet.ts.net.",
"tailscale_ips": ["100.113.80.118", "fd7a:115c:a1e0::fc35:5077"]
},
"health": []
}
}

A backend_state of Running and a 100.x address confirm the node joined the tailnet. The self.dns_name field gives the MagicDNS name the node can now be reached by from your other Tailscale machines.

Extending to the whole cluster

Once the first node checks out, apply the same configuration to the other members, then delete the file holding the key:

Fenêtre de terminal
for n in node2 node3; do
incus query -X PUT "$n:/os/1.0/services/tailscale" -d "$(cat /tmp/ts.json)"
done
shred -u /tmp/ts.json 2>/dev/null || rm -f /tmp/ts.json

Each node gets its own 100.x address and sees the others as peers. You can list the peers a node sees under state.peer: the other cluster members should appear there with online set to true. The mesh is then complete: nodes talk to each other through the encrypted tunnel, whatever their network location.

Driving the service from Operations Center

On a fleet managed by Operations Center, you do not edit each node one by one: the console proxies the service configuration of every server. You read or change a member's Tailscale service through its CLI:

Fenêtre de terminal
operations-center provisioning server os service show IncusOS01:tailscale
operations-center provisioning server os service edit IncusOS01:tailscale

The edit takes the same configuration object as the direct API (enabled, auth_key, advertised_routes and so on) and applies it to the target node. That is the centralised channel to prefer once a fleet is enrolled, with incus query still useful for an isolated node or an unmanaged cluster. This part of the API is still marked experimental in current versions.

Advertising routes towards the instances

Since the service connects the host and not the instances, making a container or a virtual machine reachable from the tailnet means advertising its subnet. You declare the routes the node offers with advertised_routes, and allow other nodes to use them with accept_routes:

Fenêtre de terminal
incus query -X PUT "node1:/os/1.0/services/tailscale" \
-d '{"config":{"enabled":true,"auth_key":"tskey-auth-...","advertised_routes":["10.167.207.0/24"],"accept_routes":true}}'

The instance subnet then becomes routable for tailnet members that accept routes, once the route has been approved on the Tailscale administration side. The service also exposes serve_enabled, serve_port and serve_service to publish the Incus API through Tailscale Serve, an option worth reserving for cases where direct HTTPS access to the interface is wanted.

Using Headscale instead

The login_server field accepts the URL of a Headscale control server, the free, self-hosted implementation of the Tailscale control plane. The configuration is identical: you just add the URL and a key generated on the Headscale side (headscale preauthkeys create).

Troubleshooting

SymptomLikely causeFix
backend_state stays empty or NeedsLoginKey invalid, expired or already consumedGenerate a reusable, unexpired key and reapply the PUT
The node gets no 100.x addressNo outbound internet access to the control planeCheck the node's default route and DNS
Failure with a Headscale login_serverURL over HTTP, or an untrusted certificateExpose Headscale over HTTPS with a trusted certificate
Instances stay unreachableThe service connects the host, not the instancesDeclare advertised_routes and approve the route on the Tailscale side

Key points

  • The Tailscale client is embedded in Incus OS: no add-on, you just enable the service.
  • All configuration goes through PUT /os/1.0/services/tailscale, per node.
  • The service connects the node, not the instances: for those, use advertised_routes.
  • Use a reusable and ephemeral key for a fleet, and delete the file holding it.
  • Reading the state returns the key in the clear: filter on state before sharing output.
  • Headscale works through login_server, but demands a publicly valid HTTPS certificate.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn