Task · TSO-0102

Split the shared TS_WIF secret: one name, two incompatible scope requirements

Description

secrets.TS_WIF_CLIENT_ID and TS_WIF_AUDIENCE are consumed by FOUR workflows that need MUTUALLY EXCLUSIVE identities, so whichever consumer is fixed last silently breaks the others.

Needs auth_keys + a tag, to JOIN THE TAILNET via the broker-token action: release-please.yml:65-66, trigger-docs-sync.yml:40-41, grafana-sync.yml:68-69 Needs all:read, to call the Tailscale API directly: live-contract.yml:68,101-102 - and line 161 says so outright, naming read scope (all:read)

A federated identity cannot hold both scope sets, so one secret cannot serve both groups.

Timeline, established 2026-08-31 from run history and secret metadata:

So Wave 3 lane B repaired a nine-day live-contract outage and, through one shared secret name, broke three workflows. Neither half was wrong in isolation; the secret name is the defect.

Identities available on the tailnet (verified live): auth_keys + tag:gha, subject repo:rknightion/tailscale2otel:ref:refs/heads/main <- broker candidate auth_keys + tag:gha, subject repo:rknightion* <- shared broker all:read, no tags, subject repo:rknightion/tailscale2otel:ref:refs/heads/main <- live-contract All three broker consumers trigger only on main (plus workflow_dispatch on main), so the repo-and-branch-pinned identity covers them and is the least-privilege choice.

Fix: give the broker path its own secret pair, leave TS_WIF_* on the all:read identity for live-contract, and add a guard test asserting no workflow using broker-token shares a WIF secret name with live-contract. Flipping the existing secret back is NOT the fix - it re-breaks live-contract.

BLOCKED ON A HUMAN: writing repository Actions secrets is refused by the agent permission classifier. The two gh secret set commands must be run by the owner.

Acceptance Criteria

Definition of Done

Implementation Notes

RESOLVED 2026-08-31, commit 43f856b (workflow repoint + guard test); secrets written by the owner at 14:04:53Z and 14:04:59Z after the agent permission classifier refused the write.

TS_WIF_BROKER_CLIENT_ID / TS_WIF_BROKER_AUDIENCE point at the auth_keys + tag:gha federated identity whose subject is pinned to this repository main branch, chosen over the repo:rknightion* wildcard because all three broker consumers trigger only on main. TS_WIF_CLIENT_ID / TS_WIF_AUDIENCE were left untouched on the all:read identity, so live-contract keeps the repair that started this.

ALL FOUR CONSUMERS PROVEN ON REAL RUNS, not inferred:

TestBrokerAndReadWIFSecretsStayDistinct in internal/ci/wifsecrets_test.go fails if a broker-token consumer and live-contract are ever pointed at the same secret name again. Negative-tested: reintroducing secrets.TS_WIF_CLIENT_ID into release-please.yml turns it red naming the workflow, restoring turns it green.

Durable lesson worth carrying: the failure mode was a 403 on CREATING AN AUTHKEY, i.e. the tailnet JOIN, which looks exactly like an OpenBao or camden outage. release-please.yml own header says “a failure here is infrastructure, not the commit”, which sent every reader down the wrong path. Check which identity the secret names before believing the infrastructure story.

Final Summary

Split the shared WIF secret so tailnet-join and API-read workflows use different identities. Verified by real runs of all four consumers (release-please at 43f856b, grafana-sync, and workflow_dispatch runs 33401560486 and 33401563117), plus a negative-tested guard test.

View the source file on GitHub