tailscale-up
Recipe card from the charly-infrastructure plugin (Images — the deployable catalog).
tailscale-up — runtime wiring for target:local hosts
Section titled “tailscale-up — runtime wiring for target:local hosts”Why this candy exists separately from tailscale
Section titled “Why this candy exists separately from tailscale”The tailscale candy installs the daemon and enables the systemd unit.
That’s a build-time concern shared by bootc images, VMs, and any
other box that wants its own tailnet identity at boot.
The --operator and --hostname settings, however, are runtime
state that:
- Cannot be applied at image build time (the daemon isn’t running, so
tailscale sethas no socket to talk to). - Are meaningless on bootc images that have no human “deploy user” matching uid 1000.
- Need re-application across hostname changes (
hostnamectl set-hostname new-namedoesn’t propagate to the tailnet device name automatically).
Mixing those concerns into the shared tailscale candy would either
break bootc consumers (with build-time errors from missing daemon
socket) or require a target-aware conditional inside the shared candy
— exactly the kind of <name>-host polymorphism that the project rulebook
“Init-system polymorphism” rule (AGENTS.md / CLAUDE.md) forbids.
tailscale-up is the runtime-config sibling: it depends on tailscale,
self-gates on systemctl is-active tailscaled, and only fires when
the daemon is actually up. Bootc consumers don’t include it.
Candy Properties
Section titled “Candy Properties”| Property | Value |
|---|---|
| Dependencies | tailscale |
| Packages | none (config-only layer) |
| Service | none (sets state on existing tailscaled.service) |
| Install files | task: (one cmd task) |
Task body
Section titled “Task body”The candy’s single cmd task:
- Starts
tailscaled.serviceviasystemctl start ... || true. The/charly-infrastructure:tailscalecandy issystemctl enable-only (no--now) because the same task body runs at image-build time where systemd isn’t PID 1 and--nowwould fail; starting the service is therefore a deploy-runtime concern that belongs in this candy. Idempotent — no-op when already running, suppressed when no live systemd is available. - Self-gates on
systemctl is-active --quiet tailscaled || exit 0so the rest of the body is a no-op in any context where the daemon isn’t running (image build, masked unit, etc.). - Resolves the human deploy user via
getent passwd 1000 | cut -d: -f1. This matches thewheel-nopasswdcandy and thedeploykit.RenderBuilderScripthelper insdk/deploykit/localpkg.go(relocated fromcharly/deploy_host_helpers.goin the W3 build-engine wave) — uid 1000 is the canonical “first human user” in this repo across CachyOS, Arch, Fedora, Debian, and Ubuntu base images.SUDO_USERis intentionally NOT used:runSudoShellcallssudo -n bash -swithout-E, so the defaultenv_resetstripsSUDO_USERon strict sudoers configurations. - Calls
tailscale set --operator="$account"(suppressed via|| trueso a logged-out daemon doesn’t fail the deploy). - Reads
/etc/hostnamedirectly (NOThostname -s— thehostnamebinary lives in Arch’sinetutilspackage, which is NOT in the minimal cloud-image base used by thecheck-arch-vmR10 bed; reading the file is binary-dependency-free and works on every distro). Callstailscale set --hostname=<short-form>(also suppressed) to keep the tailnet device name in sync with the system hostname; the short form is the value before the first dot, matchinghostname -ssemantics.
Check probes
Section titled “Check probes”Both deploy-scope probes use a three-state pattern:
- (a) Daemon down → pass-skip (image-build context, fresh test bed pre-deploy).
- (b) Daemon up + tailscale logged out → pass-skip (test bed
post-deploy, no
tailscale upperformed). - (c) Daemon up + tailscale logged in → assert.
The “logged in” detection uses tailscale status --peers=false | head -1 | grep -q "Logged out" — false means logged-in.
tailscale-up-operator-readable: when logged in, asserts that
tailscale debug prefs succeeds AND emits a non-empty
"OperatorUser": line. Calling debug prefs without sudo is itself
the proof that --operator took effect (operator-or-root is the
authorization policy).
tailscale-up-hostname-tracks-system: when logged in, asserts that
the prefs Hostname field equals hostname -s.
Used in
Section titled “Used in”local.charly-cachyos— primary consumer, applies it on every fresh CachyOS dev workstation.- Any future
target: localtemplate that needs tailnet operator permission.
NOT used in:
- Any
target: poddeployment — pod tunnels go through thetunnel: tailscalemechanism incharly.yml(handled by the host’s tailscale, not in-pod). See/charly-core:deploy“Tailscale Serve”. - Bootc images — they need fresh-boot semantics that don’t match the
deploy-runtime contract here. Use
/charly-infrastructure:tailscalealone.
Renaming an already-registered device — and why the candy doesn’t auto-fix
Section titled “Renaming an already-registered device — and why the candy doesn’t auto-fix”tailscale set --hostname=<X> updates local prefs and is what the
candy task does on every deploy. For a fresh node that has not
yet run tailscale up, the prefs Hostname is what gets registered
with the control plane on first auth — so the tailnet device name
matches /etc/hostname automatically.
For a node that was already authed before this candy landed, the
device name on the tailnet is the value registered at original
tailscale up time and DOES NOT auto-rename when prefs.Hostname
changes. This is intentional Tailscale design — names are sticky
in the control plane to keep ACL rules, DNS caches, and audit trails
stable across reconfigurations. The local prefs are updated correctly
(visible via tailscale debug prefs | grep Hostname), but
tailscale status keeps showing the old name.
The candy surfaces this divergence as a hard-fail check probe
(tailscale-up-device-name-matches-hostname). On every charly check live <deploy> against a logged-in tailnet member, the probe compares the
short form of tailscale status --self --json | .DNSName against
/etc/hostname (cut to short form). If they differ, the probe fails
with stderr text identifying the registered device, the system
hostname, and both remediation paths. The probe self-gates on daemon
state (skips on a logged-out daemon) so test beds aren’t affected.
Two remediation paths (operator action required)
Section titled “Two remediation paths (operator action required)”-
Admin-console rename at https://login.tailscale.com/admin/machines — pick the device, click the kebab menu, “Edit machine name”. Safe, no service interruption, no re-auth, preserves exit-node config and all other prefs. Recommended for any node with non-default flags.
-
sudo tailscale up --reset --hostname=$(cat /etc/hostname | cut -d. -f1)— re-registers the device with the prefs Hostname. WARNING:--resetclears every non-default flag (exit-node config, advertised routes, accept-routes, accept-DNS-override, advertise-tags, etc.). Only use this on a node where every flag is at its Tailscale default.
Why the candy doesn’t auto-rename
Section titled “Why the candy doesn’t auto-rename”The candy does not reach for tailscale up --reset automatically
because:
- It wipes operator-set prefs that the candy can’t enumerate or preserve (exit-node config, advertised routes, accept-routes are typical examples).
- It triggers a brief disconnect + re-auth attempt, which can fail on ephemeral auth keys or expire-on-rotate setups.
- It violates the project rulebook “no ad-hoc workarounds” rule: a
--resetto fix a name divergence is a sledgehammer for a problem better solved by an admin-console click or a deliberate operator-drivenup.
A proper auto-rename would require the Tailscale REST API
(PATCH https://api.tailscale.com/api/v2/device/{id}/name) plus an
operator-managed PAT or OAuth secret — significant credential-
management scope. If a future need arises for fully-automated rename
(fleet provisioning, CI runners), that’s the right path; today’s
manual remediation covers the actual single-host case.
Bring-up flow for a fresh CachyOS host
Section titled “Bring-up flow for a fresh CachyOS host”# 1. Apply the charly-cachyos template — installs everything, including# tailscale (daemon enabled) + tailscale-up (operator/hostname# setters armed for next-time-up state changes).charly bundle add charly-cachyos
# 2. Authenticate the daemon via the user's tailnet (browser SSO):sudo tailscale up
# 3. Verify operator + hostname are wired correctly:tailscale debug prefs | grep -E '"OperatorUser"|"Hostname"'tailscale status | head -2
# 4. Optional re-apply (idempotent) to confirm the runtime task takes# effect now that the daemon is logged in:charly bundle add charly-cachyosSubsequent charly bundle add charly-cachyos invocations re-run the runtime
task and re-confirm the operator + hostname state. Hostname changes
(sudo hostnamectl set-hostname new-name) propagate to the tailnet
on the next deploy.
MagicDNS
Section titled “MagicDNS”MagicDNS is a tailnet-account-level toggle, not a per-node setting.
Enable it once at https://login.tailscale.com/admin/dns ; every node
logging in to the tailnet automatically gets 100.100.100.100 as a
DNS server (visible via resolvectl status | grep '100.100.100.100')
and the *.ts.net magic search domain.
tailscale-up does not touch DNS state — tailscale up (run manually
post-deploy) handles the per-node resolver wiring automatically on
systemd-resolved hosts (which CachyOS uses).
tailscale serve for pod deploys — direct, no restart
Section titled “tailscale serve for pod deploys — direct, no restart”The --operator permission this candy wires is what makes the
direct, host-level tailscale serve workflow work without
sudo. For each pod that publishes a host port (e.g. immich-ml on
2283, ollama on 11434), one shell command on the host adds it to
the tailnet:
tailscale serve --bg --https=2283 http://127.0.0.1:2283tailscale serve --bg --https=11434 http://127.0.0.1:11434# ... one line per pod portEach invocation writes to tailscaled.state (the daemon’s local
BoltDB) and starts a listener on the tailscale interface that
forwards to the pre-existing host-loopback socket. Nothing about
the pod changes — no quadlet regen, no restart, no daemon-reload,
no port re-publish. The pod’s published port on 127.0.0.1 was
always already there; tailscale serve simply adds a new path INTO
it from the tailnet.
To list current serves and their backends:
tailscale serve statusTo remove a serve:
tailscale serve --https=2283 offThe serve config is persisted in the daemon’s state and survives restarts, so a one-time setup is durable. Re-running with the same args is idempotent (replaces the existing entry verbatim).
Compared to the per-pod tunnel: tailscale charly.yml mechanism
Section titled “Compared to the per-pod tunnel: tailscale charly.yml mechanism”/charly-core:deploy documents a tunnel: tailscale field that emits
ExecStartPost=tailscale serve ... into the pod’s quadlet. That
also works and is the right answer when (a) you want the serve
config tied to the pod’s lifecycle (added on charly config, removed
on charly remove), or (b) you’re willing to regenerate the quadlet
and restart the pod. For the more common “I have running pods, I
want them on the tailnet right now” case, the direct tailscale serve workflow above is faster, restartless, and doesn’t touch
any source-tree state.
See /charly-core:deploy “Tailscale Serve” for the per-pod tunnel
matrix and /charly-automation:sidecar for the tailscale-sidecar pattern
(an alternative when a pod needs its own tailnet identity, not just
host-level serve).
Related Skills
Section titled “Related Skills”/charly-infrastructure:tailscale— the install-side companion (daemon install + systemctl enable).tailscale-uprequire:it./charly-core:deploy—charly.ymltunnel: tailscalemechanism that consumes the--operatorpermission./charly-local:local-deploy— thetarget: localexecution model this candy is designed for./charly-local:local-spec—local.ymltemplate authoring; the canonical consumer islocal.charly-cachyos.- the
wheel-nopasswdcandy — provides the passwordless sudo used by the check probe’stailscale debug prefschain. /charly-image:layer— candy authoring reference./charly-check:check— declarative testing reference (three-state pattern used here).
When to Use This Skill
Section titled “When to Use This Skill”Use when the user asks about:
- Why
tailscale servefails with “permission denied” on a fresh charly-cachyos host. - How to keep the tailnet device name in sync with
hostname -s. - The split between the build-time tailscale install and the deploy-time tailscale wiring.
- Writing a new candy that adds runtime configuration on top of an install-side candy (this is the canonical worked example).