Skip to content

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.

Pilot — access is granted per tenant

Workload Identity is rolled out as a pilot. It is available only for tenants that hold the workload-identity entitlement, and only on managed Kubernetes clusters in such a tenant. If you don't see the Workload Identity tab on a cluster, it isn't enabled for your tenant yet — contact support to request access.

How it works

  1. You create a binding that maps a (namespace, serviceAccount) in one of your clusters to a set of least-privilege Frostmoln scopes.
  2. You annotate that Kubernetes ServiceAccount to opt it in.
  3. When a pod runs as that ServiceAccount, the cluster 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.
  4. 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 — 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

bash
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

hcl
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 2 — Annotate the ServiceAccount

Opt the ServiceAccount in by adding the annotation frostmoln.cloud/workload-identity: "true":

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: default
  annotations:
    frostmoln.cloud/workload-identity: 'true'

or imperatively:

bash
kubectl -n default annotate serviceaccount my-app \
  frostmoln.cloud/workload-identity=true

Only ServiceAccounts carrying this annotation are wired up — it is opt-in per ServiceAccount.

Step 3 — Use the credential in your pod

Run your pod as the annotated ServiceAccount. The platform 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:

bash
curl -H "Authorization: Bearer $(cat "$FROSTMOLN_WORKLOAD_CREDENTIALS_FILE")" \
  https://api.frostmoln.cloud/api/v1/...
go
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.

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, flavors
  • storage:read — read volumes and buckets
  • network: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.

bash
# 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, or terraform destroy) to revoke its access.
  • Remove the annotation from the ServiceAccount to stop injecting credentials into new pods.

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:

bash
terraform destroy -target=frostmoln_workload_identity_binding.my_app
terraform destroy

The same applies to any change that replaces the binding — cluster_id, namespace and service_account all force replacement.