Workload Identity
Workload Identity lets a pod in your managed Kubernetes cluster call the Frostmoln API using its Kubernetes ServiceAccount — with no long-lived API key stored in the pod. It is the Frostmoln equivalent of AWS IAM Roles for Service Accounts (IRSA), GCP Workload Identity Federation, and Azure Workload Identity.
A short-lived, scoped Frostmoln credential is minted for the pod on demand and refreshed automatically. Nothing secret is baked into your image or a Secret you manage.
Workload Identity needs the Workload Identity add-on on the cluster (Step 1). Without it, nothing in the cluster exchanges or injects credentials.
How it works
- You add the Workload Identity add-on to the cluster.
- You create a binding that maps a
(namespace, serviceAccount)in one of your clusters to a set of least-privilege Frostmoln scopes. - You annotate that Kubernetes ServiceAccount to opt it in.
- When a pod runs as that ServiceAccount, the add-on automatically:
- projects a short-lived Kubernetes ServiceAccount token into the pod,
- runs a small credential-helper sidecar that exchanges it for a scoped Frostmoln token, and
- writes that token to a file the app reads.
- Your app calls the Frostmoln API with the token. It is refreshed automatically before it expires (roughly hourly), so the app never handles a long-lived secret.
The exchange is one-way and outbound only: the platform never reaches into your cluster, and the pod trades a token it already holds for a Frostmoln token it is entitled to.
Step 1 — Add the Workload Identity add-on
Workload Identity is an optional cluster add-on, workload-identity.
- When you create a cluster — select Workload Identity in the portal's create form, pass
--addons workload-identitytofm kubernetes cluster create, or includeworkload-identityinaddonsonfrostmoln_kubernetes_cluster. The cluster reports running first; the add-on is applied a few minutes later. - To a running cluster —
- Portal — the cluster's Add-ons tab.
- fm CLI —bash
fm kubernetes cluster add-addons <cluster-id> --addons workload-identity - Terraform — add
workload-identitytoaddonsand apply again; the update adds it to the running cluster without replacing the cluster.
Adding it to a running cluster is not immediate: the addition starts its own reconciliation, and the add-on reaches the cluster typically within minutes. If that reconciliation cannot be started, it falls back to the platform's own reconciliation period, up to an hour.
Your nodes need outbound HTTPS
The add-on's image is pulled from registry.sweden.frostmoln.cloud when its pod starts; it is not preloaded onto your nodes. The token exchange calls api.frostmoln.cloud. Your worker nodes need outbound HTTPS to both. The platform does not check either; see Create a cluster.
Step 2 — Create a binding
A binding grants a specific ServiceAccount a specific set of permissions. Those come either from scopes on the binding itself, or from an access policy attached to it — see Grant with an access policy below. Create the binding in the portal, with the fm CLI, or with Terraform.
Portal
On the cluster's detail page, open the Workload Identity tab and choose Add binding. Enter the namespace and service account name, pick the scopes to grant, and save.
fm CLI
fm kubernetes workload-identity binding create \
--cluster <cluster-id> \
--namespace default \
--service-account my-app \
--scope compute:read --scope storage:read
# List and delete
fm kubernetes workload-identity binding list --cluster <cluster-id>
fm kubernetes workload-identity binding delete <binding-id>Terraform
resource "frostmoln_workload_identity_binding" "my_app" {
cluster_id = frostmoln_kubernetes_cluster.prod.id
namespace = "default"
service_account = "my-app"
scopes = [
"compute:read",
"storage:read",
]
}Step 3 — Annotate the ServiceAccount
Opt the ServiceAccount in by adding the annotation frostmoln.cloud/workload-identity: "true":
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: default
annotations:
frostmoln.cloud/workload-identity: 'true'or imperatively:
kubectl -n default annotate serviceaccount my-app \
frostmoln.cloud/workload-identity=trueOnly ServiceAccounts carrying this annotation are wired up — it is opt-in per ServiceAccount.
Pods that are already running
From add-on v0.3.0-1, annotating a ServiceAccount restarts its running pods so they receive the credential. They are evicted one at a time, honouring PodDisruptionBudgets, and their controller recreates them. Bare pods and Job pods are not restarted; restart them yourself. Earlier versions only inject pods created after the annotation.
Step 4 — Use the credential in your pod
Run your pod as the annotated ServiceAccount. The add-on injects everything automatically; your app only needs to read the token from the file named by the FROSTMOLN_WORKLOAD_CREDENTIALS_FILE environment variable and send it as a bearer token:
curl -H "Authorization: Bearer $(cat "$FROSTMOLN_WORKLOAD_CREDENTIALS_FILE")" \
https://api.frostmoln.cloud/api/v1/...token, _ := os.ReadFile(os.Getenv("FROSTMOLN_WORKLOAD_CREDENTIALS_FILE"))
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(string(token)))The file is refreshed in place before the token expires, so re-read it (rather than caching its contents) for long-running processes.
Credential in a Secret
Some tools read credentials from a Kubernetes Secret rather than a file — the External Secrets Operator is one. For them, annotate the ServiceAccount with frostmoln.cloud/workload-identity-secret: <secret-name>:
apiVersion: v1
kind: ServiceAccount
metadata:
name: eso
namespace: default
annotations:
frostmoln.cloud/workload-identity-secret: frostmoln-tokenThe add-on then keeps an Opaque Secret of that name in the same namespace, holding the exchanged Frostmoln token under the key token. It still needs a binding for that namespace and ServiceAccount — grant secrets:read for ESO. The frostmoln.cloud/workload-identity: "true" annotation is not needed; the two modes are independent.
- Lifetime — the token lasts about an hour and is refreshed automatically at about 80% of its lifetime.
- Labels — the Secret carries
app.kubernetes.io/managed-by: frostmoln-workload-identityandexternal-secrets.io/type: webhook, so ESO's webhook provider can read it. - Never overwrites — if a Secret with that name already exists and the add-on did not create it, nothing is written. Pick another name.
- Removal — removing or renaming the annotation deletes the Secret, and so does deleting the ServiceAccount.
- Not in platform namespaces — such as
kube-system.
Anyone who can read Secrets in the namespace can read the token
Keep the Secret in a namespace with tight RBAC. Unlike an API key, the token is short-lived.
Scopes
Workload identities are least-privilege by design. A binding's scopes must be explicit and concrete — the * (all) and <resource>:* (e.g. compute:*) wildcards are rejected. Grant only the scopes the workload actually needs, for example:
compute:read— read instances, images, flavorsstorage:read— read volumes and bucketsnetwork:read— read VPCs, subnets, security groups
To broaden or narrow access later, update the binding's scopes — the change takes effect on the next token refresh.
Grant with an access policy
Scopes are coarse: compute:read grants reads across every instance, image and flavor. An access policy can name individual resources, add conditions such as a source-IP range, and deny explicitly. For a workload that should touch one bucket from one network, a policy expresses what a scope cannot.
To use one, create the binding without scopes and attach a policy to it.
# Create a binding with no scopes
fm kubernetes workload-identity binding create \
--cluster <cluster-id> --namespace ops --service-account reaper
# Attach a policy to it
fm iam policy attach <policy-id> --type workload_identity --id <binding-id>In the portal, leave the scope picker empty when adding the binding, then attach a policy under Settings → Access Policies. In Terraform, omit scopes and add a frostmoln_iam_policy_attachment.
A binding with no scopes lists as no scopes — neither the portal nor fm can tell one granted by an attached policy from one granting nothing, so neither claims it does. Three things are worth knowing:
- It is inert until a policy is attached. With nothing granting it, the token exchange refuses to mint rather than issuing a credential that grants nothing — the pod's credential helper reports an error instead of getting a dead token.
- A binding can never be left granting nothing. Removing its last remaining grant — its scopes, or its last policy — is rejected. Delete the binding instead.
- While a binding has both scopes and a policy, the scopes are what apply. The policy's additional permissions take effect only once the scopes are removed, so verify after removing them, not after attaching the policy.
Access policies require the IAM access policies feature on your tenant.
Token lifetime
The minted Frostmoln token is short-lived (about one hour) and refreshed automatically by the injected credential helper. There is no long-lived secret to rotate: revoking access is a matter of deleting the binding (or removing the annotation), after which no new tokens are minted for that ServiceAccount.
Removing access
- Delete the binding (portal,
fm kubernetes workload-identity binding delete, orterraform destroy) to revoke its access. - Remove the annotation from the ServiceAccount to stop injecting credentials into new pods.
Removing the add-on
Remove the add-on from the Add-ons tab, with fm kubernetes cluster remove-addons <cluster-id> --addons workload-identity, or by dropping the key from addons in Terraform. Its webhook is removed first: pods already running keep their injected sidecar until they are restarted. Synced Secrets are no longer refreshed, and their token expires within about an hour.
Deleting the binding is also how you remove a workload's access entirely — a binding cannot be emptied down to granting nothing.
Terraform: destroy a policy-granted binding before its attachment
Because a binding can never be left with no grant, detaching its only policy is rejected. terraform destroy removes the attachment first (it depends on the binding) and therefore fails. Destroy the binding first; that removes its attachments with it:
terraform destroy -target=frostmoln_workload_identity_binding.my_app
terraform destroyThe same applies to any change that replaces the binding — cluster_id, namespace and service_account all force replacement.