Skip to content

Repository files navigation

Thalassa Workload Identity Controller

Experimental (v1beta1). APIs and behaviour may change.

Provisions Thalassa Cloud IAM (service account, federated identity, policy binding) for a Kubernetes ServiceAccount so pods can exchange a projected JWT for a Thalassa bearer token via Workload Identity Federation.

Does not create the cluster OIDC identity provider. On Thalassa-managed clusters that IdP already exists (kubernetes_cluster_id=<cluster identity>). For other clusters, create the IdP via the API first.

Prerequisites

Item Notes
Kubernetes cluster with OIDC IdP registered in Thalassa Same cluster identity you use with tcloud
tcloud CLI Bootstrap the controller’s own WIF once
Helm 3 Charts under chart/ (CRDs + controller)
cert-manager (optional) Only if you enable the pod mutator webhook with default TLS

Collect these IDs before install:

ORG_ID=...           # Thalassa organisation
CLUSTER_ID=...       # Thalassa Kubernetes cluster identity
PROJECT_ID=...       # optional; empty = organisation-root IAM for policyRef

Deployment topologies

Topology Where the reconciler runs Where the webhook runs
Thalassa-managed Kubernetes Control plane (Thalassa-operated; customers do not install it) In-cluster (values-webhook-only.yaml)
Self-managed In-cluster In-cluster (values-incluster.yaml)

Default chart values enable controller only (controller.enabled=true, webhook.enabled=false). Admission must call an in-cluster Service, so the mutator is always deployed in the workload cluster when used.

Same container image; Helm renders separate Deployments with distinct ServiceAccounts and RBAC.

Install

1. Bootstrap the controller service account (once)

Only needed when you run the reconciler (self-managed / controller-only). Skip for webhook-only on managed clusters.

tcloud iam workload-identity-federation bootstrap kubectl \
  --cluster "$CLUSTER_ID" \
  --namespace thalassa-system \
  --service-account thalassa-workload-identity-controller \
  --role iam:FullAccess \
  --scope api:read,api:write

Save the printed Thalassa service account ID as CONTROLLER_THALASSA_SA_ID.

2. Install CRDs, then the chart

Install the CRDs chart first (same pattern as thalassa-dbaas-manager-crds). The release workflow publishes both charts under oci://ghcr.io/thalassa-cloud/charts/.

Controller only (default):

helm upgrade --install thalassa-workload-identity-controller-crds \
  ./chart/thalassa-workload-identity-controller-crds \
  --namespace thalassa-system --create-namespace

helm upgrade --install thalassa-workload-identity-controller \
  ./chart/thalassa-workload-identity-controller \
  --namespace thalassa-system --create-namespace \
  --set thalassa.organisation="$ORG_ID" \
  --set thalassa.clusterIdentity="$CLUSTER_ID" \
  --set thalassa.serviceAccountId="$CONTROLLER_THALASSA_SA_ID" \
  --set thalassa.project="$PROJECT_ID"   # omit or empty for org-root policies

Self-managed (controller + webhook):

helm upgrade --install thalassa-workload-identity-controller \
  ./chart/thalassa-workload-identity-controller \
  --namespace thalassa-system --create-namespace \
  -f chart/thalassa-workload-identity-controller/values-incluster.yaml \
  --set thalassa.organisation="$ORG_ID" \
  --set thalassa.clusterIdentity="$CLUSTER_ID" \
  --set thalassa.serviceAccountId="$CONTROLLER_THALASSA_SA_ID"

Webhook only (managed Kubernetes; CRDs if not preinstalled):

helm upgrade --install thalassa-workload-identity-webhook \
  ./chart/thalassa-workload-identity-controller \
  --namespace thalassa-system --create-namespace \
  -f chart/thalassa-workload-identity-controller/values-webhook-only.yaml
Helm value Required Purpose
thalassa.organisation when controller enabled Organisation for API calls / token exchange
thalassa.clusterIdentity when controller enabled Lookup cluster OIDC IdP
thalassa.serviceAccountId when controller enabled Controller’s Thalassa SA (from bootstrap)
thalassa.project no Scope for policyRef (X-Project-Identity); empty = org root
controller.enabled no Default true; reconciler Deployment
webhook.enabled no Default false; mutator Deployment + MWC (needs cert-manager by default)
webhook.failurePolicy no Default Ignore; use Fail with HA
metrics.secure no Default true (HTTPS + authn/authz on :8443)
enableServiceMonitor no Prometheus Operator ServiceMonitor(s)
controller.enableIdentityConfigMap no Sync wif-<sa> ConfigMap for all Ready bindings
controller.allowedPolicies / allowedRoles no Allowlists (empty = allow any)
controller.watchNamespaces no Limit watch scope; empty = all namespaces

HA webhook overlay (2 replicas, PDB, failurePolicy: Fail):

-f chart/thalassa-workload-identity-controller/values-webhook-ha.yaml

Without cert-manager, provide a TLS secret (tls.crt / tls.key) and set:

--set webhook.certManager.enabled=false \
--set webhook.tls.secretName=my-webhook-certs

Flux examples: deploy/flux/. Chart details: chart/thalassa-workload-identity-controller/README.md.

3. Verify

# Controller Deployment (when enabled)
kubectl -n thalassa-system rollout status deploy/thalassa-workload-identity-controller-controller
kubectl -n thalassa-system logs deploy/thalassa-workload-identity-controller-controller -c manager -f

# Webhook Deployment (when enabled)
kubectl -n thalassa-system rollout status deploy/thalassa-workload-identity-controller-webhook

Day-2: bind a workload

Desired fields are at the root of the CR (like RoleBinding), not under spec.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: observability-proxy
  namespace: monitoring
---
apiVersion: iam.thalassa.cloud/v1beta1
kind: WorkloadIdentityBinding
metadata:
  name: observability-proxy
  namespace: monitoring
serviceAccountName: observability-proxy
policyRef: observability:RemoteWriteAccess   # prefer stable policy identity
# roleRef: ...                                # transitional alternative
# scopes: ["openid"]                          # default if omitted
# identityConfigMap: true                     # optional per-binding ConfigMap
# deleteResources: true                       # delete Thalassa resources on CR delete

Full example (including pod label): examples/serviceaccount.yaml.

Check status

kubectl get wib -n monitoring
kubectl describe wib observability-proxy -n monitoring

Ready when status.phase=Ready and condition Ready=True. Useful status fields: serviceAccountID, organisationID, projectID, policyID, federatedIdentityID.

JWT subject used for federation: system:serviceaccount:<namespace>:<name>.

Projected token audience for exchange: https://api.thalassa.cloud (override via thalassa.projectedToken.audience / trusted audiences).

How pods get credentials

Token exchange needs organisation ID + Thalassa service account ID + projected JWT. When Ready, the controller always annotates the Kubernetes ServiceAccount:

Annotation When
thalassa.cloud/wif.organisation-id always
thalassa.cloud/wif.service-account-id always
thalassa.cloud/wif.project-id only if controller --project / thalassa.project is set

Pick one consumption path:

A. Pod mutator webhook (recommended)

  1. Helm: enable webhook (values-incluster.yaml, values-webhook-only.yaml, or --set webhook.enabled=true)
  2. Label the pod: thalassa.cloud/wif.use: "true" (label, not annotation — required for webhook objectSelector)
  3. Webhook injects env + volume when the SA annotations are present:
Env Value
THALASSA_ORGANISATION_ID from SA annotation
THALASSA_SERVICE_ACCOUNT_ID from SA annotation
THALASSA_PROJECT_ID if annotated
THALASSA_SUBJECT_TOKEN_FILE /var/run/secrets/thalassa/token

Default webhook failurePolicy is Ignore (scheduling continues if the webhook is down). Set webhook.failurePolicy=Fail for stricter clusters.

B. Identity ConfigMap

Set identityConfigMap: true on the binding (and/or controller.enableIdentityConfigMap=true). Creates ConfigMap wif-<serviceAccountName>:

Key Required for exchange
organisation-id yes
service-account-id yes
project-id no

Mount it next to a projected SA token (see comments in the example). Turning the flag off does not delete existing ConfigMaps; deleting the binding still GC’s owner-referenced ones.

C. Explicit config

Copy IDs from kubectl get wib … -o yaml / SA annotations into your app Helm values; mount only the projected JWT.

RBAC (who may create bindings)

Limit workloadidentitybindings write access to platform / GitOps identities. App teams can own ServiceAccounts without cloud IAM rights.

apiGroups: ["iam.thalassa.cloud"]
resources: ["workloadidentitybindings"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

CEL on the CR rejects missing policyRef/roleRef and wildcards (*) at apply time. Optional controller allowlists further restrict which policies/roles may be referenced.

Controller Thalassa permissions

The controller calls the Thalassa API as its bootstrapped SA. Recommended policy: iam:FullAccess at the project and/or org-root scope where it manages bindings. Docs: API authorization.

API area Access needed
Service accounts list, create, delete
Federated identities list, create, update, delete
Federated identity providers list only (never create)
IAM policies & bindings get/list policies; list/create/delete bindings
Organisation roles & bindings only if using roleRef

Controller federated identity scopes: at least api:read and api:write.

Development

make generate   # deepcopy + CRDs → chart/...-crds/templates
make test
make lint
make build

About

Kubernetes controller that provisions Thalassa Cloud service accounts, federated identities, and IAM policy bindings. Making it easy to support workloads that can authenticate with projected JWTs via Workload Identity Federation

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages