A netavark network plugin that bridges rootless container traffic onto a Tailscale network (tailnet) via tsnet. It creates a TUN device inside a container's network namespace and makes containers appear as ephemeral Tailscale nodes. Designed for use with podman.
- Linux (all source files are
//go:build linux) - Go 1.25+
go build -o netavark-tailscale-plugin .The binary operates in two modes:
- Plugin mode (
info,create,setup,teardown): short-lived netavark plugin protocol handlers (JSON stdin/stdout) - Daemon mode (
daemon): long-running tsnet process started by the plugin
podman run --network tailscale-net ...
-> netavark invokes: netavark-tailscale-plugin setup /run/netns/xxx < JSON
-> plugin writes config.json to state dir
-> plugin starts daemon as a child process
-> daemon creates TUN in container netns, starts tsnet, configures interface
-> daemon writes ready.json (IPs, MAC)
-> plugin polls for ready.json, builds StatusBlock, returns JSON
-> container stops
-> netavark invokes: netavark-tailscale-plugin teardown /run/netns/xxx < JSON
-> plugin stops daemon, cleans up state dir
Config merges three layers (later overrides earlier):
- Network options (
podman network create --opt key=value) - Per-container options (quadlet
NetworkOptions=key=value) - Environment variables (
TS_AUTHKEY,TS_HOSTNAME, etc.)
| Variable | Required | Description |
|---|---|---|
TS_AUTHKEY |
Yes | Tailscale auth key for the ephemeral node. Cleared from the process environment after reading. Use ephemeral, single-use auth keys. |
TS_HOSTNAME |
Yes | Hostname to register on the tailnet |
TS_EXIT_NODE |
No | Exit node to route traffic through: an exit-node IP (e.g. 100.64.0.1), or auto:any to auto-select the best available exit node and re-pick if it goes offline |
TS_CONTROL_URL |
No | Custom control server URL |
Note: Tailscale enables logtail
by default. Diagnostic logs may be uploaded to log.tailscale.com during
runtime. This is standard Tailscale behavior.
Install the plugin binary where netavark can find it, and tell podman where to look:
# Copy the binary
sudo install -m 755 netavark-tailscale-plugin /usr/local/lib/netavark-plugins/
# Tell netavark where to find plugins
cat <<'EOF' | sudo tee /etc/containers/containers.conf
[network]
netavark_plugin_dirs = ["/usr/local/lib/netavark-plugins"]
EOFCreate the podman network (once per host):
podman network create --driver netavark-tailscale-plugin tailscaleThis shows a complete setup using quadlet systemd units and tailmint for ephemeral auth key minting. Each container gets a fresh, single-use Tailscale auth key at startup and appears as its own node on the tailnet.
- A Tailscale OAuth client with
a tag (e.g.
tag:containers) that the client is authorized to create devices under tailmintinstalled at/usr/local/bin/tailmint- OAuth credentials in
/etc/tailscale/oauth.env:
TS_API_CLIENT_ID=your-oauth-client-id
TS_API_CLIENT_SECRET=tskey-client-...- A sudoers rule so the container user can mint keys (replace
nginx-demowith your user):
nginx-demo ALL=(root) NOPASSWD: /usr/local/bin/tailmint -config /etc/tailscale/oauth.env -tag tag\:containers -hostname * -output /run/user/*/ts-authkeys/*.env
Place this at ~/.config/containers/systemd/nginx-demo.container:
[Unit]
Description=nginx on tailnet
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/nginx:latest
ContainerName=nginx-demo
Network=tailscale
[Service]
# Mint a fresh ephemeral auth key before each start
ExecStartPre=mkdir -p %t/ts-authkeys
ExecStartPre=sudo /usr/local/bin/tailmint -config /etc/tailscale/oauth.env -tag tag:containers -hostname %N -output %t/ts-authkeys/%N.env
ExecStartPre=podman network create --ignore --driver netavark-tailscale-plugin tailscale
# Load the minted key into the service environment
EnvironmentFile=-%t/ts-authkeys/%N.env
# TS_HOSTNAME and TS_AUTHKEY are read by netavark-tailscale-plugin on the
# host side, so they go in [Service], not [Container]
Environment=TS_HOSTNAME=%N
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=default.targetStart it:
systemctl --user daemon-reload
systemctl --user start nginx-demoThe container joins the tailnet as nginx-demo and is reachable from any device
on the same tailnet.
These are set as environment variables in [Service] or as network/container
options:
| Variable | Option | Required | Description |
|---|---|---|---|
TS_AUTHKEY |
— | Yes | Tailscale auth key (use ephemeral, single-use) |
TS_HOSTNAME |
hostname |
Yes | Node hostname on the tailnet |
TS_EXIT_NODE |
exit_node |
No | Exit node to route traffic through: an exit-node IP, or auto:any to auto-select the best available exit node (mirrors tailscale set --exit-node) |
TS_CONTROL_URL |
control_url |
No | Custom Tailscale control server URL |
TS_TLS_CERTS_DIR |
tls_certs_dir |
No | Absolute directory where the daemon writes cert.pem + key.pem (Let's Encrypt via Tailscale) and refreshes them. Bind-mount it into the container so the app can terminate TLS itself. |
# Unit tests (no root required)
go test -run 'TestValidateMTU|TestFdTUNCloseEvents|TestPluginJSON|TestStatusBlock|TestConfigMerge|TestExitNodePrefs' -v ./...
# Integration tests with fake control server (no root required)
go test -run 'TestTsnetConnectsToControl|TestTwoNodesCanCommunicate|TestExitNodeConfig' -v ./...
# Namespace tests (requires root)
sudo go test -run 'TestCreateTUNInNamespace|TestConfigureInterface' -v ./...
# Full end-to-end (requires root)
sudo go test -run 'TestFullFlow' -v ./...BSD 3-Clause — see LICENSE.