Skip to content

feat: tailscale-operator Workload Identity(beta) - infra#7333

Open
boxp wants to merge 12 commits intomainfrom
T-20260302-009-tailscale-wif
Open

feat: tailscale-operator Workload Identity(beta) - infra#7333
boxp wants to merge 12 commits intomainfrom
T-20260302-009-tailscale-wif

Conversation

@boxp
Copy link
Owner

@boxp boxp commented Mar 2, 2026

See PR commits for changes. Related: boxp/lolice#514

@boxp-tfaction

This comment has been minimized.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30028b1819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +53
depends_on = [
tailscale_acl.this,
aws_s3_object.oidc_discovery,
aws_s3_object.oidc_jwks,

Choose a reason for hiding this comment

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

P1 Badge Add public-read policy dependency before WIF creation

tailscale_federated_identity.k8s_operator currently waits for the two S3 objects but not for aws_s3_bucket_policy.k8s_oidc_public_read, so on a fresh apply Terraform can start WIF creation while the issuer documents are still private and Tailscale’s issuer validation gets a 403. This makes applies flaky/failing in the exact bootstrap path this commit introduces; include the bucket policy in depends_on to guarantee the issuer URL is publicly readable before creating the federated identity.

Useful? React with 👍 / 👎.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction

This comment has been minimized.

@github-actions

This comment has been minimized.

@boxp-tfaction
Copy link
Contributor

boxp-tfaction bot commented Mar 13, 2026

Plan Result (terraform/tailscale/lolice)

CI link

Plan: 9 to add, 1 to change, 0 to destroy.
  • Create
    • aws_s3_bucket.k8s_oidc
    • aws_s3_bucket_policy.k8s_oidc_public_read
    • aws_s3_bucket_public_access_block.k8s_oidc
    • aws_s3_bucket_server_side_encryption_configuration.k8s_oidc
    • aws_s3_object.oidc_discovery
    • aws_s3_object.oidc_jwks
    • aws_ssm_parameter.operator_wif_audience
    • aws_ssm_parameter.operator_wif_client_id
    • tailscale_federated_identity.k8s_operator
  • Update
    • tailscale_acl.this
Change Result (Click me)
  # aws_s3_bucket.k8s_oidc will be created
  + resource "aws_s3_bucket" "k8s_oidc" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "lolice-k8s-oidc"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # aws_s3_bucket_policy.k8s_oidc_public_read will be created
  + resource "aws_s3_bucket_policy" "k8s_oidc_public_read" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + policy = (known after apply)
      + region = "ap-northeast-1"
    }

  # aws_s3_bucket_public_access_block.k8s_oidc will be created
  + resource "aws_s3_bucket_public_access_block" "k8s_oidc" {
      + block_public_acls       = true
      + block_public_policy     = false
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "ap-northeast-1"
      + restrict_public_buckets = false
    }

  # aws_s3_bucket_server_side_encryption_configuration.k8s_oidc will be created
  + resource "aws_s3_bucket_server_side_encryption_configuration" "k8s_oidc" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + rule {
          + blocked_encryption_types = []

          + apply_server_side_encryption_by_default {
              + sse_algorithm     = "AES256"
                # (1 unchanged attribute hidden)
            }
        }
    }

  # aws_s3_object.oidc_discovery will be created
  + resource "aws_s3_object" "oidc_discovery" {
      + acl                    = (known after apply)
      + arn                    = (known after apply)
      + bucket                 = (known after apply)
      + bucket_key_enabled     = (known after apply)
      + checksum_crc32         = (known after apply)
      + checksum_crc32c        = (known after apply)
      + checksum_crc64nvme     = (known after apply)
      + checksum_sha1          = (known after apply)
      + checksum_sha256        = (known after apply)
      + content                = (known after apply)
      + content_type           = "application/json"
      + etag                   = (known after apply)
      + force_destroy          = false
      + id                     = (known after apply)
      + key                    = ".well-known/openid-configuration"
      + kms_key_id             = (known after apply)
      + region                 = "ap-northeast-1"
      + server_side_encryption = (known after apply)
      + storage_class          = (known after apply)
      + tags_all               = (known after apply)
      + version_id             = (known after apply)
    }

  # aws_s3_object.oidc_jwks will be created
  + resource "aws_s3_object" "oidc_jwks" {
      + acl                    = (known after apply)
      + arn                    = (known after apply)
      + bucket                 = (known after apply)
      + bucket_key_enabled     = (known after apply)
      + checksum_crc32         = (known after apply)
      + checksum_crc32c        = (known after apply)
      + checksum_crc64nvme     = (known after apply)
      + checksum_sha1          = (known after apply)
      + checksum_sha256        = (known after apply)
      + content                = jsonencode(
            {
              + keys = []
            }
        )
      + content_type           = "application/json"
      + etag                   = (known after apply)
      + force_destroy          = false
      + id                     = (known after apply)
      + key                    = "openid/v1/jwks"
      + kms_key_id             = (known after apply)
      + region                 = "ap-northeast-1"
      + server_side_encryption = (known after apply)
      + storage_class          = (known after apply)
      + tags_all               = (known after apply)
      + version_id             = (known after apply)
    }

  # aws_ssm_parameter.operator_wif_audience will be created
  + resource "aws_ssm_parameter" "operator_wif_audience" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + description    = "Tailscale WIF audience for the Kubernetes Operator (not a secret)"
      + has_value_wo   = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/lolice/tailscale/operator-wif-audience"
      + region         = "ap-northeast-1"
      + tags           = {
          + "Project" = "lolice"
          + "Purpose" = "tailscale-k8s-operator-wif"
        }
      + tags_all       = {
          + "Project" = "lolice"
          + "Purpose" = "tailscale-k8s-operator-wif"
        }
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + value_wo       = (write-only attribute)
      + version        = (known after apply)
    }

  # aws_ssm_parameter.operator_wif_client_id will be created
  + resource "aws_ssm_parameter" "operator_wif_client_id" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + description    = "Tailscale WIF client ID for the Kubernetes Operator (not a secret)"
      + has_value_wo   = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/lolice/tailscale/operator-wif-client-id"
      + region         = "ap-northeast-1"
      + tags           = {
          + "Project" = "lolice"
          + "Purpose" = "tailscale-k8s-operator-wif"
        }
      + tags_all       = {
          + "Project" = "lolice"
          + "Purpose" = "tailscale-k8s-operator-wif"
        }
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + value_wo       = (write-only attribute)
      + version        = (known after apply)
    }

  # tailscale_acl.this will be updated in-place
  ~ resource "tailscale_acl" "this" {
      ~ acl = jsonencode(
          ~ {
              ~ tagOwners     = {
                  + "tag:k8s"           = [
                      + "tag:k8s-operator",
                    ]
                    # (3 unchanged attributes hidden)
                }
                # (2 unchanged attributes hidden)
            }
        )
        id  = "dca925a2-b72b-a210-25af-3ccbdb1f4046"
    }

  # tailscale_federated_identity.k8s_operator will be created
  + resource "tailscale_federated_identity" "k8s_operator" {
      + audience    = (known after apply)
      + created_at  = (known after apply)
      + description = "lolice k8s-operator WIF"
      + id          = (known after apply)
      + issuer      = (known after apply)
      + scopes      = [
          + "auth_keys",
          + "devices:core",
          + "services",
        ]
      + subject     = "system:serviceaccount:tailscale-operator:operator"
      + tags        = [
          + "tag:k8s-operator",
        ]
      + updated_at  = (known after apply)
      + user_id     = (known after apply)
    }

Plan: 9 to add, 1 to change, 0 to destroy.

Changes to Outputs:
  + k8s_oidc_issuer_url        = (known after apply)
  + k8s_operator_wif_audience  = (known after apply)
  + k8s_operator_wif_client_id = (known after apply)

@github-actions
Copy link
Contributor

Ansible Plan Results

Mode: --check --diff (dry run)

⚠️ Changes detected


shanghai-1: control-plane

Host OK Changed Skipped Failed Unreachable
shanghai-1 72 1 12 0 0

1 changed

Changed Tasks (1)
# Task Module
1 user_management : Update package cache unknown

shanghai-1: node-shanghai-1

Host OK Changed Skipped Failed Unreachable
shanghai-1 9 0 0 0 0

No changes

shanghai-2: control-plane

Host OK Changed Skipped Failed Unreachable
shanghai-2 72 1 12 0 0

1 changed

Changed Tasks (1)
# Task Module
1 user_management : Update package cache unknown

shanghai-2: node-shanghai-2

Host OK Changed Skipped Failed Unreachable
shanghai-2 9 0 0 0 0

No changes

shanghai-3: control-plane

Host OK Changed Skipped Failed Unreachable
shanghai-3 72 1 12 0 0

1 changed

Changed Tasks (1)
# Task Module
1 user_management : Update package cache unknown

shanghai-3: node-shanghai-3

Host OK Changed Skipped Failed Unreachable
shanghai-3 9 0 0 0 0

No changes


Plan executed on all nodes in parallel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants