Skip to content

Codify Tailscale ACL with tag:homelab scaffolding - #7

Merged
nickvigilante merged 1 commit into
mainfrom
codify-tailscale-acl
May 11, 2026
Merged

Codify Tailscale ACL with tag:homelab scaffolding#7
nickvigilante merged 1 commit into
mainfrom
codify-tailscale-acl

Conversation

@nickvigilante

Copy link
Copy Markdown
Owner

Summary

Brings the tailnet policy file under OpenTofu management. No functional behavior change in this PR.

What lands:

  • tailscale_acl.main resource — imports the current ACL into Tofu state and re-serializes it via jsonencode. The comments and example scaffolding from the starter template go away; the actual rules are preserved verbatim.
  • New scaffolding for Phase 2:
    • tagOwners for tag:homelab — only autogroup:admin can apply the tag, so a random tailnet member can't spoof a homelab node.
    • SSH rule autogroup:member -> tag:homelab for nickv and root — no-op today because no device wears the tag.

Phase 2 (intentionally separate PR)

After this lands:

  1. On gandalf: sudo tailscale up --advertise-tags=tag:homelab
  2. Approve the tag in the Tailscale admin UI
  3. Open a follow-up PR that replaces the *:* allow-all rule with restrictive admin/tag-scoped rules

Why split: doing both at once leaves the operator unable to reach gandalf if anything's off — the *:* rule disappears before any device has the tag the new rules grant access to.

OAuth scope dependency

The opentofu-homelab OAuth client now needs the acl (read+write) scope (added in the admin UI before this PR was opened — the apply step would fail without it).

Test plan

  • tofu import tailscale_acl.main acl brings the existing ACL into state
  • tofu plan shows 0 to add, 1 to change, 0 to destroy — change is the comments-to-JSON reserialization plus the two additive pieces
  • No acls, ssh, or nodeAttrs rule loses content in the diff
  • tofu apply succeeds
  • Tailscale API confirms the four top-level keys are present (acls, ssh, nodeAttrs, tagOwners)
  • Re-running tofu plan after apply is a no-op

Brings the tailnet policy file under OpenTofu management. Keeps the
existing default-allow stance and SSH/Funnel defaults intact while
adding forward-looking structure that no-ops until devices wear the
new tag:

- `tagOwners` for `tag:homelab` (only autogroup:admin can apply it)
- SSH rule `autogroup:member -> tag:homelab` for `nickv` and `root`

Phase 2 (separate PR after gandalf advertises tag:homelab) will replace
the `*:*` acls rule with restrictive admin/tag-scoped rules. Tightening
in the same change as introducing tagging would leave the operator
unable to reach gandalf via Tailscale.

OAuth client `opentofu-homelab` needs the `acl` (read+write) scope —
added in the Tailscale admin UI before this lands.

Applied locally with `tofu import tailscale_acl.main acl` followed by
in-place update: 0 added, 1 changed, 0 destroyed.
@github-actions

Copy link
Copy Markdown

homelab tofu plan

Plan output
tailscale_dns_nameservers.global: Refreshing state... [id=34619e51-87ec-b5df-c078-fd2e3181d9c5]
tailscale_dns_preferences.main: Refreshing state... [id=ad9f64d5-f380-ae42-4811-7604bfdb5bcd]
tailscale_acl.main: Refreshing state... [id=acl]
data.github_user.self: Reading...
github_repository.managed["vale-languagetool"]: Refreshing state... [id=vale-languagetool]
github_repository.managed["puzzles"]: Refreshing state... [id=puzzles]
github_repository.managed["tools"]: Refreshing state... [id=tools]
github_repository.managed["homelab"]: Refreshing state... [id=homelab]
github_repository.managed["dotfiles"]: Refreshing state... [id=dotfiles]
github_repository.managed["infrastructure"]: Refreshing state... [id=infrastructure]
github_repository.managed["tuile"]: Refreshing state... [id=tuile]
github_repository.managed["passgen"]: Refreshing state... [id=passgen]
github_repository.managed["styleguide"]: Refreshing state... [id=styleguide]
github_repository.managed["docs"]: Refreshing state... [id=docs]
data.github_user.self: Read complete after 1s [id=13631471]
github_actions_secret.aws_secret_access_key: Refreshing state... [id=infrastructure:AWS_SECRET_ACCESS_KEY]
github_actions_secret.github_app_installation_id: Refreshing state... [id=infrastructure:TF_GITHUB_APP_INSTALLATION_ID]
github_repository_vulnerability_alerts.alerts["styleguide"]: Refreshing state... [id=877056782]
github_actions_secret.github_app_id: Refreshing state... [id=infrastructure:TF_GITHUB_APP_ID]
github_repository_vulnerability_alerts.alerts["vale-languagetool"]: Refreshing state... [id=874539266]
github_actions_secret.tailscale_oauth_client_id: Refreshing state... [id=infrastructure:TAILSCALE_OAUTH_CLIENT_ID]
github_repository_environment.homelab_apply: Refreshing state... [id=infrastructure:homelab-apply]
github_actions_secret.tailscale_oauth_client_secret: Refreshing state... [id=infrastructure:TAILSCALE_OAUTH_CLIENT_SECRET]
github_actions_secret.github_app_private_key: Refreshing state... [id=infrastructure:TF_GITHUB_APP_PRIVATE_KEY]
github_actions_secret.aws_access_key_id: Refreshing state... [id=infrastructure:AWS_ACCESS_KEY_ID]
github_repository_vulnerability_alerts.alerts["puzzles"]: Refreshing state... [id=1221351905]
github_repository_vulnerability_alerts.alerts["infrastructure"]: Refreshing state... [id=1234973546]
github_repository_vulnerability_alerts.alerts["tools"]: Refreshing state... [id=177344790]
github_repository_vulnerability_alerts.alerts["docs"]: Refreshing state... [id=1213739034]
github_repository_vulnerability_alerts.alerts["dotfiles"]: Refreshing state... [id=692319991]
github_repository_vulnerability_alerts.alerts["homelab"]: Refreshing state... [id=1235102549]
github_repository_vulnerability_alerts.alerts["tuile"]: Refreshing state... [id=1209980965]
github_repository_vulnerability_alerts.alerts["passgen"]: Refreshing state... [id=1007162400]
github_branch_protection.main["styleguide"]: Refreshing state... [id=BPR_kwDONEbTDs4Ek1Lj]
github_branch_protection.main["tools"]: Refreshing state... [id=BPR_kwDOCpIRFs4CjUwo]
github_branch_protection.main["puzzles"]: Refreshing state... [id=BPR_kwDOSMxZ4c4Ek1Lo]
github_branch_protection.main["vale-languagetool"]: Refreshing state... [id=BPR_kwDONCBpAs4Ek1LO]
github_branch_protection.main["tuile"]: Refreshing state... [id=BPR_kwDOSB7YJc4Ek1LN]
github_branch_protection.main["homelab"]: Refreshing state... [id=BPR_kwDOSZ4rVc4Ek1bm]
github_branch_protection.main["docs"]: Refreshing state... [id=BPR_kwDOSFgwGs4Ek1Ll]
github_branch_protection.main["passgen"]: Refreshing state... [id=BPR_kwDOPAgUIM4Ek1Lp]
github_branch_protection.main["infrastructure"]: Refreshing state... [id=BPR_kwDOSZwzas4Ek1LQ]
github_branch_protection.main["dotfiles"]: Refreshing state... [id=BPR_kwDOKUP2984EclBF]

No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.

Warning: Deprecated attribute

  on github.tf line 32, in resource "github_repository" "managed":
  32:     ignore_changes = [vulnerability_alerts]

The attribute "vulnerability_alerts" is deprecated. Refer to the provider
documentation for details.

Warning: Argument is deprecated

  with github_actions_secret.tailscale_oauth_client_id,
  on github.tf line 85, in resource "github_actions_secret" "tailscale_oauth_client_id":
  85:   plaintext_value = var.tailscale_oauth_client_id

Use value.

(and 6 more similar warnings elsewhere)

@nickvigilante
nickvigilante merged commit 167d64b into main May 11, 2026
1 check passed
@nickvigilante
nickvigilante deleted the codify-tailscale-acl branch May 11, 2026 23:54
nickvigilante added a commit that referenced this pull request May 12, 2026
Phase 2 of the Tailscale ACL codification (Phase 1 was PR #7). Gandalf
now wears `tag:homelab`, so the policy can drop the allow-all rule:

- `autogroup:member -> tag:homelab:*`: your tailnet devices reach
  Jellyfin, Pi-hole, Uptime Kuma on gandalf
- `tag:homelab -> tag:homelab:*`: future cluster nodes can talk to
  each other (k3s agent join, kubelet API)

SSH and Funnel rules unchanged. The `*:*` allow-all rule is gone — a
device that isn't a tailnet member and isn't tagged `tag:homelab` can't
reach anything on the homelab side.

Verified with `curl -v http://100.92.2.25/` from a tailnet macbook:
Traefik returned the expected 404 (no IngressRoute matches a bare-IP
Host), proving the Tailscale layer routed the connection through.
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.

1 participant