Tailscale: IaC Management

Level 2 (Topic) — Terraform provider, ACL as code, OAuth client model.

Concept

Tailscale resources are managed as infrastructure-as-code in ephemeral-castle/tailscale/. The Terraform provider manages ACL policy, tailnet settings, and OAuth clients. Auth keys are generated dynamically at runtime rather than stored in state.

Key Files

FileRole
main.tfACL policy, tailnet settings, OAuth client
acl.jsonTag ownership and ACL rules (source of truth)
variables.tfInput variables (tailnet, API key)
outputs.tfExported outputs
versions.tfProvider version constraints
setup.shInitial tailnet setup helper

ACL as Code

The ACL policy is defined in acl.json and applied via tailscale_acl resource:

resource "tailscale_acl" "tazlab" {
  acl = file("${path.module}/acl.json")
}

This is a hard replace-on-create resource — any drift in the JSON file is corrected on the next terraform apply.

OAuth Client Model

The bootstrap OAuth client generates short-lived auth keys at runtime instead of persisting them:

resource "tailscale_oauth_client" "bootstrap" {
  description = "tazlab-bootstrap"
  scopes      = ["auth_keys", "devices"]
  tags        = ["tag:tazpod"]
}

A second OAuth client is used by the Tailscale Kubernetes Operator for service exposure:

resource "tailscale_oauth_client" "k8s_operator" {
  description = "tazlab-k8s-operator"
  scopes      = ["devices", "auth_keys", "services"]
  tags        = ["tag:k8s-operator"]
}

The k8s_operator client requires the services scope (bug #19471 in v1.96.x) — without it, the operator’s Tailnet CR validation fails with InvalidOAuth, blocking proxy creation for Ingress/LoadBalancer exposure.

The operator daemon (tazpod-tailscale-up) uses OAuth client credentials from ~/secrets/ to mint 1-hour auth keys on demand. This avoids storing long-lived keys in Terraform state or encrypted vault.

Tailnet Settings

resource "tailscale_tailnet_settings" "tazlab" {
  devices_approval_on       = false
  devices_auto_updates_on   = false
  devices_key_duration_days = 180
}

See Also