This repository contains a Terraform scaffold for managing a Tailscale tailnet.
The tracked files are sanitized for public-repo readiness. Live exports, local tfvars, Terraform state, and credentials are ignored.
Manage these items with Terraform:
- Full tailnet policy file via
tailscale_acl. - Tailnet settings via
tailscale_tailnet_settings. - MagicDNS, global DNS, split DNS, and search paths.
- Device tags and enabled subnet routes once device names are known.
- Webhooks and log streaming if you send events to Slack, Discord, SIEM, or storage.
- Short-lived auth keys only for automation bootstrap, preferably generated by CI rather than committed.
Keep these out of Terraform state when possible:
- Long-lived pre-auth keys.
- OAuth client secrets.
- Per-machine runtime flags such as advertised routes; Terraform can enable advertised routes, but each node still has to advertise them locally.
- Prefer
grantsover legacyaclsfor new policy work. Grants are the forward-looking syntax and support network plus application-layer capabilities. - Keep the policy deny-by-default. Avoid broad
*to*rules except as a temporary import bridge. - Require policy tests for every important allow and deny path. Terraform validates the policy before applying when tests are present.
- Put servers, routers, CI runners, and app connectors behind tags. Avoid user-owned identity for unattended infrastructure.
- Use
tagOwnersnarrowly. Admin-owned tags are fine for small tailnets; larger setups should delegate tag ownership by group. - Use
autoApproversfor subnet routers and exit nodes only with tagged infrastructure, not individual users, where possible. - For Tailscale SSH, separate self-device access from server access. Use
checkfor privileged server logins and avoid blanketroot. - Turn on externally managed ACLs once Terraform owns the policy to prevent console edits from drifting.
- Turn on device approval, device auto-updates, key expiry, HTTPS, and network flow logging if your plan and client fleet support them.
- Route changes through pull requests with
terraform fmt,terraform validate, andterraform plan.
- Export the current live state with
make export-current. - Generate ignored local values with
make write-current-tfvars, or editterraform/terraform.tfvarsmanually. - Import existing managed resources before applying.
- Run
terraform planand resolve drift. - Add policy tests while preserving current behavior.
- Remove broad/stale rules in a separate change.
- Enable
acls_externally_managed_onafter the policy matches production.
For live drift-free adoption, terraform/terraform.tfvars can point at the ignored exported policy:
policy_file = "../exports/current/acl.hujson"The generated terraform/terraform.tfvars file is ignored because it can contain real device names, tailnet names, tags, and internal Tailscale IPs.
Run Terraform through the wrapper so .env.local is sourced without committing credentials:
make init
make planmake fmt formats tracked Terraform files only. Use make fmt-local if you also want to format ignored local files such as terraform/terraform.tfvars.
If make validate fails while loading the Tailscale provider schema, refresh the local Terraform runtime/provider cache before treating it as an HCL problem. A known local failure mode is a provider binary that installs but cannot instantiate.
The Tailscale provider needs one of:
TAILSCALE_API_KEYTAILSCALE_OAUTH_CLIENT_IDplusTAILSCALE_OAUTH_CLIENT_SECRET
Generate current import commands from the exported device snapshot:
make print-importsOr import the current exported resources directly:
make import-currentCore imports:
terraform -chdir=terraform import tailscale_acl.policy acl
terraform -chdir=terraform import tailscale_tailnet_settings.this tailnet_settings
terraform -chdir=terraform import tailscale_dns_preferences.this dns_preferences
terraform -chdir=terraform import 'tailscale_dns_nameservers.global[0]' dns_nameserversDevice-specific imports use stable node IDs where possible:
terraform -chdir=terraform import 'tailscale_device_tags.by_name["server.example.ts.net"]' nodeidCNTRL
terraform -chdir=terraform import 'tailscale_device_subnet_routes.by_name["router.example.ts.net"]' nodeidCNTRL- Tailscale tailnet policy syntax: https://tailscale.com/docs/reference/syntax/policy-file
- Tailscale grants syntax: https://tailscale.com/docs/reference/syntax/grants
- Tailscale Terraform provider: https://github.com/tailscale/terraform-provider-tailscale