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

@@ -34,7 +34,9 @@ spec:
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if or .Values.oauth.clientSecret .Values.oauth.audience }}
volumes:
{{- if .Values.oauth.clientSecret }}
- name: oauth
{{- with .Values.oauthSecretVolume }}
{{- toYaml . | nindent 10 }}
@@ -42,6 +44,17 @@ spec:
secret:
secretName: operator-oauth
{{- end }}
{{- else }}
- name: oidc-jwt
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: {{ .Values.oauth.audience }}
expirationSeconds: 3600
path: token
{{- end }}
{{- end }}
containers:
- name: operator
{{- with .Values.operatorConfig.securityContext }}
@@ -72,10 +85,15 @@ spec:
value: {{ .Values.loginServer }}
- name: OPERATOR_INGRESS_CLASS_NAME
value: {{ .Values.ingressClass.name }}
{{- if .Values.oauth.clientSecret }}
- name: CLIENT_ID_FILE
value: /oauth/client_id
- name: CLIENT_SECRET_FILE
value: /oauth/client_secret
{{- else if .Values.oauth.audience }}
- name: CLIENT_ID
value: {{ .Values.oauth.clientId }}
{{- end }}
{{- $proxyTag := printf ":%s" ( .Values.proxyConfig.image.tag | default .Chart.AppVersion )}}
- name: PROXY_IMAGE
value: {{ coalesce .Values.proxyConfig.image.repo .Values.proxyConfig.image.repository }}{{- if .Values.proxyConfig.image.digest -}}{{ printf "@%s" .Values.proxyConfig.image.digest}}{{- else -}}{{ printf "%s" $proxyTag }}{{- end }}
@@ -100,10 +118,18 @@ spec:
{{- with .Values.operatorConfig.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if or .Values.oauth.clientSecret .Values.oauth.audience }}
volumeMounts:
{{- if .Values.oauth.clientSecret }}
- name: oauth
mountPath: /oauth
readOnly: true
{{- else }}
- name: oidc-jwt
mountPath: /var/run/secrets/tailscale/serviceaccount
readOnly: true
{{- end }}
{{- end }}
{{- with .Values.operatorConfig.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View File

@@ -1,7 +1,7 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
{{ if and .Values.oauth .Values.oauth.clientId -}}
{{ if and .Values.oauth .Values.oauth.clientId .Values.oauth.clientSecret -}}
apiVersion: v1
kind: Secret
metadata:

View File

@@ -1,13 +1,20 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
# Operator oauth credentials. If set a Kubernetes Secret with the provided
# values will be created in the operator namespace. If unset a Secret named
# operator-oauth must be precreated or oauthSecretVolume needs to be adjusted.
# This block will be overridden by oauthSecretVolume, if set.
oauth: {}
# clientId: ""
# clientSecret: ""
# Operator oauth credentials. If unset a Secret named operator-oauth must be
# precreated or oauthSecretVolume needs to be adjusted. This block will be
# overridden by oauthSecretVolume, if set.
oauth:
# The Client ID the operator will authenticate with.
clientId: ""
# If set a Kubernetes Secret with the provided value will be created in
# the operator namespace, and mounted into the operator Pod. Takes precedence
# over oauth.audience.
clientSecret: ""
# The audience for oauth.clientId if using a workload identity federation
# OAuth client. Mutually exclusive with oauth.clientSecret.
# See https://tailscale.com/kb/1581/workload-identity-federation.
audience: ""
# URL of the control plane to be used by all resources managed by the operator.
loginServer: ""