cmd/k8s-operator: support workload identity federation

The feature is currently in private alpha, so requires a tailnet feature
flag. Initially focuses on supporting the operator's own auth, because the
operator is the only device we maintain that uses static long-lived
credentials. All other operator-created devices use single-use auth keys.

Testing steps:

* Create a cluster with an API server accessible over public internet
* kubectl get --raw /.well-known/openid-configuration | jq '.issuer'
* Create a federated OAuth client in the Tailscale admin console with:
  * The issuer from the previous step
  * Subject claim `system:serviceaccount:tailscale:operator`
  * Write scopes services, devices:core, auth_keys
  * Tag tag:k8s-operator
* Allow the Tailscale control plane to get the public portion of
  the ServiceAccount token signing key without authentication:
  * kubectl create clusterrolebinding oidc-discovery \
      --clusterrole=system:service-account-issuer-discovery \
      --group=system:unauthenticated
* helm install --set oauth.clientId=... --set oauth.audience=...

Updates #17457

Change-Id: Ib29c85ba97b093c70b002f4f41793ffc02e6c6e9
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
Tom Proctor
2025-10-05 02:10:50 +01:00
parent 1ed117dbc0
commit d4c5b278b3
7 changed files with 272 additions and 34 deletions

View File

@@ -164,22 +164,24 @@ func main() {
runReconcilers(rOpts)
}
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
// CLIENT_ID_FILE and CLIENT_SECRET_FILE environment variables to authenticate
// with Tailscale.
// initTSNet initializes the tsnet.Server and logs in to Tailscale. If CLIENT_ID
// is set, it authenticates to the Tailscale API using the federated OIDC workload
// identity flow. Otherwise, it uses the CLIENT_ID_FILE and CLIENT_SECRET_FILE
// environment variables to authenticate with static credentials.
func initTSNet(zlog *zap.SugaredLogger, loginServer string) (*tsnet.Server, tsClient) {
var (
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
clientID = defaultEnv("CLIENT_ID", "") // Used for workload identity federation.
clientIDPath = defaultEnv("CLIENT_ID_FILE", "") // Used for static client credentials.
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "") // Used for static client credentials.
hostname = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
kubeSecret = defaultEnv("OPERATOR_SECRET", "")
operatorTags = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
)
startlog := zlog.Named("startup")
if clientIDPath == "" || clientSecretPath == "" {
startlog.Fatalf("CLIENT_ID_FILE and CLIENT_SECRET_FILE must be set")
if clientID == "" && (clientIDPath == "" || clientSecretPath == "") {
startlog.Fatalf("CLIENT_ID_FILE and CLIENT_SECRET_FILE must be set") // TODO(tomhjp): error message can mention WIF once it's publicly available.
}
tsc, err := newTSClient(context.Background(), clientIDPath, clientSecretPath, loginServer)
tsc, err := newTSClient(zlog.Named("ts-api-client"), clientID, clientIDPath, clientSecretPath, loginServer)
if err != nil {
startlog.Fatalf("error creating Tailscale client: %v", err)
}