tailscale/tailscale · error

failed to create OAuth client for second tailnet: %w

Error message

failed to create OAuth client for second tailnet: %w

What it means

bootstrapClient.Keys().CreateOAuthClient failed creating the operator's OAuth client on the second tailnet, with the same request shape as the first tailnet (scopes auth_keys, devices:core, services; tag:k8s-operator). This is the credential that ends up in the second-tailnet-credentials Secret used by the multi-tailnet operator tests. Failure means devcontrol returned an error: the tag is not permitted (the second tailnet's policy from the previous step lacks tagOwners for tag:k8s-operator), a scope was rejected, the bootstrap token is invalid or expired, or the endpoint is unsupported for org-created tailnets in the running devcontrol build.

Source

Thrown at cmd/k8s-operator/e2e/setup.go:357

		}
		logger.Info("HTTPS settings configured for second tailnet")

		// Set ACLs for second tailnet.
		if err = bootstrapClient.PolicyFile().Set(ctx, string(requiredACLs), ""); err != nil {
			return 0, fmt.Errorf("failed to set policy file: %w", err)
		}

		logger.Info("ACLs configured for second tailnet")

		// Create an OAuth client for the second tailnet to be used
		// by the k8s-operator.
		secondKey, err := bootstrapClient.Keys().CreateOAuthClient(ctx, tailscale.CreateOAuthClientRequest{
			Scopes:      []string{"auth_keys", "devices:core", "services"},
			Tags:        []string{"tag:k8s-operator"},
			Description: "k8s-operator client for e2e tests",
		})
		if err != nil {
			return 0, fmt.Errorf("failed to create OAuth client for second tailnet: %w", err)
		}
		secondClientID = secondKey.ID
		secondClientSecret = secondKey.Key

		secondTSClient, err = tailscaleClientFromSecret(ctx, "http://localhost:31544", secondClientID, secondClientSecret)
		if err != nil {
			return 0, fmt.Errorf("failed to set up second tailnet client: %w", err)
		}

	} else {
		if ok := testCAs.AppendCertsFromPEM(leStagingRootX1); !ok {
			return 0, fmt.Errorf("failed to parse Let's Encrypt staging root")
		}

		clientSecret = os.Getenv("TS_API_CLIENT_SECRET")
		if clientSecret == "" {
			return 0, fmt.Errorf("must use --devcontrol or set TS_API_CLIENT_SECRET to an OAuth client suitable for the operator")
		}

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Verify the previous log line 'ACLs configured for second tailnet' appeared; if not, investigate the policy step first.
  2. Check devcontrol logs or the wrapped HTTP body to see whether a tag or a scope was rejected.
  3. Rebuild and restart devcontrol from the same commit as the tests; rerun the suite.
  4. Retry the run once for transient errors; the whole flow re-executes deterministically.
Defensive patterns

Strategy: retry

Validate before calling

// Assert policy is in place on the second tailnet before requesting tagged OAuth clients:
// the ACL from the previous step must define tagOwners for tag:k8s-operator, otherwise this call fails with 400.

Try / catch

// Go: one bounded retry for 5xx/transport; keep 4xx fatal with the body.
secondKey, err := bootstrapClient.Keys().CreateOAuthClient(ctx, req)
if err != nil && isTransient(err) {
    time.Sleep(2 * time.Second)
    secondKey, err = bootstrapClient.Keys().CreateOAuthClient(ctx, req)
}
if err != nil {
    return 0, fmt.Errorf("failed to create OAuth client for second tailnet: %w", err)
}

Prevention

When it happens

Trigger: The second tailnet's ACL apply in the previous step did not take effect, so tag:k8s-operator has no owner and the server rejects the tag grant; the bootstrap token expired between steps; the devcontrol version does not support OAuth client creation scoped to an org tailnet; a transient 5xx from devcontrol.

Common situations: Editing requiredACLs without keeping tagOwners in sync; version skew between devcontrol and the harness; flaky devcontrol restarts in CI.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/26ff67bee10d3221. Report an issue: GitHub.