Skip to content

Join the tailnet from a cidata tailscale_authkey - #97

Merged
dhh merged 2 commits into
quattrofrom
tailscale-authkey
Aug 7, 2026
Merged

Join the tailnet from a cidata tailscale_authkey#97
dhh merged 2 commits into
quattrofrom
tailscale-authkey

Conversation

@dhh

@dhh dhh commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What

A tailscale_authkey file on the autoinstall drive gets the installed machine onto the tailnet on its own. Combined with authorized_keys, this completes the disposable-VM story: create VM, boot, walk away, ssh user@machine over the tailnet from anywhere.

How

  • The package is bundled, not fetched. tailscale rides in the offline mirror (~10MB on the ISO, added via builder/archinstall.packages) and is installed during the ordinary package phase — but only when the key file is present, so a stock install stays stock. Fetching at first boot was rejected: pacman -Sy tailscale is a partial upgrade, and pacman -Syu tailscale means an unattended full system upgrade racing the user's first login. The offline mirror is a coherent snapshot; installing from it keeps the target consistent and the install offline-capable.
  • The join is a first-boot unit, not an install step. tailscale up needs a running tailscaled and there is no systemd in the chroot, so the installer only stages: the key at /etc/tailscale/authkey (root, 0600), tailscaled.service enabled, and a background omarchy-tailscale-join.service (Type=simple — a oneshot wanted by multi-user.target would hold the whole boot hostage, since target units implicitly gain After= for their Wants=) that retries tailscale up until the network is really there (network-online.target can be reached before real connectivity exists). Cleanup is sequenced inside the script after tailscale up succeeds: the key is deleted and the unit disables itself; on a boot with no connectivity both survive, so a machine installed offline joins on the first boot that has connectivity.
  • Firewall: ufw allow in on tailscale0, the same write-the-rule-then-ignore-the-chroot-exit-code dance as configure_ssh_access — without it the node joins and is then unreachable over the tailnet.
  • The node appears under the hostname from user_configuration.json; nothing extra to configure. An empty or ambiguous key file fails the install loudly, as does a target without the tailscale binary (an ISO built before the package was bundled).

Full rationale in plans/tailscale.md.

Testing

  • ./test/all passes: new test_configure_tailscale.py mirrors the SSH phase tests (staging, permissions, unit content, ufw chroot behavior, loud failures), and cidata-load-test.sh covers the new optional file.
  • Not yet exercised against a built ISO — needs a build from this branch since beta1's offline mirror doesn't carry tailscale.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q

A tailscale_authkey file on the autoinstall drive gets the installed
machine onto the tailnet on its own: the tailscale package is bundled in
the offline mirror and installed only when the key is present, the key is
staged root-only at /etc/tailscale/authkey, and a oneshot first-boot unit
runs tailscale up once the network is really there. On success the key is
deleted and the unit disables itself; on failure both survive to retry on
the next boot, so an offline install joins whenever it first gets
connectivity. ufw allows traffic in on tailscale0 via the same
write-the-rule-then-ignore-the-chroot-exit-code dance as the SSH phase.

Bundling rather than fetching at first boot is deliberate: pacman -Sy
tailscale on a fresh machine is a partial upgrade, and pacman -Syu means
an unattended full system upgrade racing the user's first login. The
offline mirror is a coherent snapshot; installing from it keeps the
target consistent and the install offline-capable. See plans/tailscale.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q
@dhh

dhh commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot Review.

Copilot AI commented Aug 7, 2026

Copy link
Copy Markdown

@copilot Review.

The implementation is clean and well-structured. Here's the review:

Correctness

  • Package is installed from the offline mirror during arch_install_system while the mirror bind-mount is still active — timing is right.
  • configure_tailscale is a strict no-op unless tailscale_authkey_path is set, so stock installs are unaffected.
  • Auth key is staged at 0600 root-only (/etc/tailscale/authkey), deleted by ExecStartPost on success, kept on failure so the join retries on the next boot that has connectivity.
  • ConditionPathExists on the unit means even if the unit is left enabled after a successful join (the systemctl disable races nothing dangerous), it won't re-run once the key is gone.
  • ufw dance (check=False + verify user.rules) mirrors configure_ssh_access exactly — correct, since netfilter is unreachable in a chroot.
  • Early failure when the tailscale binary is missing prevents a "succeeds but never joins" outcome.

Retry logic in the unit
The until tailscale up …; do sleep 10; done loop under TimeoutStartSec=10min is bounded per-boot. On timeout systemd marks the unit failed, ExecStartPost is skipped (key and unit survive), and the join retries next boot. This is the intended behaviour and it's correct.

Hostname
tailscale up without --hostname picks up the system hostname, which the installer has already set from user_configuration.json by the time the unit runs on first boot. No issue.

Tests
All 13 unit tests pass. Coverage includes happy path, permissions, unit content, no-$ assertion, ufw non-zero exit tolerance, and all loud-failure cases (missing binary, empty file, comment-only, multi-key).

No issues to address — the PR is ready.

Copilot finished work on behalf of dhh August 7, 2026 13:01
Review caught that the join unit was Type=oneshot wanted by
multi-user.target: target units implicitly gain After= for their Wants=,
so an offline machine's boot -- SDDM included -- stalled for the full
10-minute timeout, and once timed out the join never retried within that
boot. Type=simple counts as started the moment it forks, so boot proceeds
while the join retries in the background for as long as the boot lasts.
Cleanup moves inside the script, sequenced after tailscale up succeeds,
which preserves the retry-across-boots behavior: key and unit survive a
boot with no connectivity, and the key never outlives a successful join.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q
Copilot AI lite review requested due to automatic review settings August 7, 2026 13:09
@dhh
dhh merged commit 0339b13 into quattro Aug 7, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional Tailscale autoinstall support: if a tailscale_authkey file is present on the cidata drive, the installer stages a first-boot join (including UFW allowance) and installs the tailscale package from the ISO’s offline mirror, keeping stock installs unchanged.

Changes:

  • Add tailscale_authkey as an optional cidata input, plumb it through the install wrapper/env/context, and install tailscale only when the key is present.
  • Stage /etc/tailscale/authkey, enable tailscaled, install a first-boot join unit, and add a UFW rule for tailscale0.
  • Add unit tests for configure_tailscale, extend cidata load tests, and document the feature (README + plan).

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py Installs tailscale conditionally and implements configure_tailscale (key staging, unit install, UFW rule validation).
configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py Registers the new “Configuring Tailscale” phase in the orchestrator sequence.
configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py Adds tailscale_authkey_path sourced from OMARCHY_INSTALL_TAILSCALE_AUTHKEY_FILE via _optional_path.
configs/airootfs/usr/local/bin/omarchy-iso-install Adds --tailscale-authkey-file flag mapping to env for the orchestrator.
configs/airootfs/usr/local/bin/omarchy-cidata-load Copies tailscale_authkey from cidata when present.
configs/airootfs/root/.automated_script.sh Always passes --tailscale-authkey-file /root/tailscale_authkey (no-op when absent).
builder/archinstall.packages Adds tailscale to the bundled/offline mirror package set.
test/test_configure_tailscale.py New unit tests for the configure_tailscale phase behavior and failure modes.
test/cidata-load-test.sh Extends cidata load coverage to include the optional tailscale_authkey file.
README.md Documents the new optional tailscale_authkey autoinstall input and behavior.
plans/tailscale.md Adds the design/acceptance criteria write-up for the feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1395 to +1399
# `tailscale up` needs a running tailscaled and there is no systemd in the
# chroot, so the install only stages: the key, the enabled services, and a
# oneshot first-boot unit that performs the join once the network is really
# there. The package itself was installed from the offline mirror during
# arch_install_system -- nothing is fetched at boot.
Comment thread README.md
| `user_email_address.txt` | No | Git email |
| `user_encrypt_installation.txt` | No | `true` when `user_configuration.json` carries a `disk_encryption` block; defaults to false |
| `authorized_keys` | No | SSH public keys in sshd's own format, one per line |
| `tailscale_authkey` | No | Tailscale auth key; the machine joins your tailnet on first boot |
dhh added a commit that referenced this pull request Aug 15, 2026
The consumer-secure-boot plan now reflects the current repo: installs
already boot mkinitcpio-built UKIs via Limine with pacman hooks
regenerating them, so the remaining work is signing, not UKI plumbing.
The Microsoft shim submission is called out as the calendar-bound
critical path, and DKMS/NVIDIA module signing is promoted to a v1
blocker. BitLocker guidance matches the shipped decrypt-not-suspend
policy from #105.

Removed plans that have shipped: tailscale (#97), autoinstall,
oem-install (#98/#107), dual-boot, and protected-partition-install
(dual-boot half shipped, restore half superseded by factory snapshots).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1HofkzWQwCMZSYAxm1tGe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants