Skip to content

TerraformClusterIdentity

A TerraformClusterIdentity names a Secret of cloud credentials and the namespaces allowed to use it. It is cluster-scoped. A platform admin creates it, along with the Secret. TerraformCluster.spec.identityRef, TerraformCluster.spec.defaults.identityRef and the spec.identityRef of a TerraformMachine or TerraformMachinePool reference it by name. The manager copies the Secret into each allowed namespace that uses the identity and mounts that copy into the Jobs it runs there.

Property Value
API version infrastructure.cluster.x-k8s.io/v1alpha1
Scope Cluster
Created by A platform admin, with kubectl apply
Referenced by TerraformCluster (spec.identityRef, spec.defaults.identityRef), TerraformMachine and TerraformMachinePool (spec.identityRef)
Finalizers None. The delete webhook protects an identity in use instead
Short names None
Categories cluster-api
Status subresource Yes

Example

A minimal identity for one namespace, and the Secret it points at:

identity.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: TerraformClusterIdentity
metadata:
  name: aws
spec:
  secretRef:
    name: aws
    namespace: captf-system
  allowedNamespaces:
    list:
      - team-a
---
apiVersion: v1
kind: Secret
metadata:
  name: aws
  namespace: captf-system
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: REPLACE_WITH_ACCESS_KEY_ID
  AWS_SECRET_ACCESS_KEY: REPLACE_WITH_SECRET_ACCESS_KEY

Full example

Every spec field set. list and selector are combined: a namespace is allowed when either matches.

identity.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
kind: TerraformClusterIdentity
metadata:
  name: aws
spec:
  secretRef:
    name: aws # (1)!
    namespace: captf-system # (2)!
  allowedNamespaces:
    list: # (3)!
      - team-a
      - team-b
    selector: # (4)!
      matchLabels:
        captf.io/identity-aws: "true"
  1. The Secret’s name. A DNS subdomain, up to 253 characters.
  2. The Secret’s namespace. Required, and any namespace works. It does not have to be an allowed namespace.
  3. Up to 100 namespace names. At least one if list is set.
  4. A Kubernetes label selector over namespace labels. Namespaces it matches are allowed in addition to those in list.

Spec

spec must have at least one property, and spec.secretRef is required. Every field is mutable: the CRD and webhook mark none immutable.

Field Type Description
spec object The desired state: the credentials Secret and who may use it. Required.
spec.secretRef object The credentials Secret. Required.
spec.allowedNamespaces object Which namespaces may reference the identity. Default: unset, which allows no namespace. Mutable.

Secret reference

Field Type Description
spec.secretRef.name string Name of the Secret. Required. Mutable. Range: 1 to 253 characters. Allowed values: a DNS subdomain (lowercase alphanumerics, - and ., starting and ending with an alphanumeric).
spec.secretRef.namespace string Namespace of the Secret. Required. Mutable. Default: none; unlike some Kubernetes references it is never defaulted to the manager’s namespace. Range: 1 to 63 characters. Allowed values: a DNS label.

The Secret may live in any namespace. The admission webhook requires that the user who creates or changes the identity may get it, which keeps an identity from exposing a Secret its author cannot read. See Validation.

Allowed namespaces

spec.allowedNamespaces decides which namespaces may use the identity. The manager checks it each time an object resolves its credentials, so changing it takes effect on the next reconcile of every object that uses the identity.

Field Type Description
spec.allowedNamespaces.list array of string Names of allowed namespaces. Range: 1 to 100 items, each 1 to 63 characters. Allowed values: DNS labels. Items are unique (a set).
spec.allowedNamespaces.selector LabelSelector Allows every namespace whose labels match. An empty selector ({}) matches every namespace, including ones that do not exist yet.

How the values combine:

allowedNamespaces Allowed namespaces
Unset None
list only Exactly the listed namespaces
selector only Namespaces whose labels match
list and selector The union: a namespace in list, or one the selector matches
selector: {} Every namespace
{} (neither field) Rejected at admission

The empty object is rejected rather than read as “every namespace” or “no namespace”, because the most permissive value should not look like the least. Write selector: {} for every namespace. A list of zero items also fails validation (MinItems=1).

spec.allowedNamespaces.selector is a standard Kubernetes LabelSelector applied to the labels of the Namespace object, so matchLabels and matchExpressions both work. CAPTF reads the namespace of the referencing object at reconcile time. It does not list namespaces ahead of time. A selector that is invalid, or a namespace that does not exist, denies access; a failed read of the namespace leaves the check Unknown (IdentityCheckFailed) and retries.

identity.yaml
spec:
  allowedNamespaces:
    selector:
      matchExpressions:
        - key: environment
          operator: In
          values: ["staging", "production"]

Anyone who can label a namespace can opt it in

A selector trusts namespace labels. Anyone who may edit the labels on a namespace can bring it under the selector, so restrict who may label namespaces, or prefer list.

The credentials Secret

The Secret is a plain Opaque Secret. Which keys it holds is defined by the module that uses the identity: name the keys for the variables its providers read, not for CAPTF. For the keys each cloud expects, see the AWS, Azure, GCP, OCI and OpenStack module pages. For creating it, see Identities and Credentials.

A Job never mounts the source Secret. The manager copies it into the namespace of each object that uses the identity, as captf-creds-<identity> (labeled captf.io/mirrored: "true", annotated with captf.io/source-hash; see Annotations and labels). The Job gets the copy twice:

  • As environment variables, one per key, through envFrom. A key starting with TF_ or KUBE_ is not passed to the environment, except the variables the Job sets itself.
  • As read-only files, one per key, under /var/run/captf/credentials/<key> with file mode 0440. Every key appears as a file, including TF_ and KUBE_ keys.

The identity never owns the source Secret. The mirror is owned by the objects that use it, and the manager deletes it when none remain.

Every Job in an allowed namespace carries these credentials

Anything that runs in the Job, including the module’s Terraform code and its providers, can read them. Grant an identity only the cloud permissions the modules need, and allow only the namespaces that should have them. See the security model.

Status

The manager sets status; you do not write it. status has at least one property when present.

Field Type Description
status object The observed state: whether the credentials Secret exists, and where it is mirrored.
status.namespaces array of string Namespaces where a mirror of the credentials Secret currently exists and an object uses the identity. Sorted. Range: up to 1000 items, each 1 to 63 characters.
status.conditions array of Condition The identity’s conditions. Only Ready is set. Range: up to 32 items. Keyed by type.
status.conditions[].type string The condition type: Ready.
status.conditions[].status string True, False or Unknown.
status.conditions[].reason string A machine-readable reason: SecretFound or SecretNotFound. See Conditions.
status.conditions[].message string A human-readable detail. For SecretNotFound it names the Secret as namespace/name.
status.conditions[].lastTransitionTime time When status last changed.
status.conditions[].observedGeneration integer The metadata.generation the condition was computed from.

A namespace appears in status.namespaces only when it holds the mirror Secret, correctly labeled and annotated, and at least one object there uses the identity. A Secret with the mirror’s name that someone else created does not count.

Example status
status:
  conditions:
    - type: Ready
      status: "True"
      reason: SecretFound
      observedGeneration: 2
      lastTransitionTime: "2026-10-01T14:03:11Z"
  namespaces:
    - team-a
    - team-b

Conditions

An identity sets one condition, Ready.

Status Meaning
True The credentials Secret named by spec.secretRef exists (SecretFound).
False The Secret does not exist (SecretNotFound). Objects that use this identity start no Job until it does.
Unknown Not set by the identity controller.

Ready says nothing about which namespaces are allowed or whether a mirror exists; the objects that reference the identity report that through their IdentityAllowed and CredentialsMirrored conditions. The controller does not watch the source Secret, so it re-reads it every five minutes and notices a Secret created or deleted out of band within that time. It also emits IdentitySecretNotFound and IdentitySecretFound events when Ready changes. Every reason and its meaning is in Conditions.

Printer columns

kubectl get terraformclusteridentities shows:

Column Source
Secret .spec.secretRef.name
Namespace .spec.secretRef.namespace
Ready The status of the Ready condition
Age .metadata.creationTimestamp

Validation

The CRD schema and the validating admission webhook enforce these rules. The webhook runs on create, update and delete, and a request fails if the webhook is unreachable.

Schema rules:

  • spec must have at least one property, and spec.secretRef.name and spec.secretRef.namespace are required, with the length and character limits in Secret reference.
  • spec.allowedNamespaces must set list, selector or both. The empty object is rejected with a message telling you to write selector: {}.
  • spec.allowedNamespaces.list has 1 to 100 unique items, each a DNS label.
  • status.conditions has at most 32 items and status.namespaces at most 1000.

Webhook rules, on create and update:

  • spec.secretRef.name and spec.secretRef.namespace must be non-empty.
  • Each spec.allowedNamespaces.list item must be a valid DNS label.
  • spec.allowedNamespaces.selector must parse as a label selector.
  • The requesting user must be allowed to get the Secret. The webhook sends a SubjectAccessReview for verb get on secrets in spec.secretRef.namespace, named spec.secretRef.name, as the requester (user, groups and extra attributes). If it is denied, the request fails with user "<name>" may not get Secret <namespace>/<name>. On create the check always runs. On update it runs only when spec.secretRef or spec.allowedNamespaces changed, in either direction, because the first chooses a Secret and the second chooses where it is copied. Changing only labels or annotations skips it.

Webhook rules, on delete:

  • The delete is refused while any TerraformCluster, TerraformMachine or TerraformMachinePool, in any namespace, resolves to the identity through its own identityRef or a fallback. An object being deleted still counts. The message names one such object.
  • The delete is refused while status.namespaces is not empty. The message lists the namespaces.

The TerraformCluster, TerraformMachine and TerraformMachinePool webhooks do not look up the identity they reference. A cluster must set a non-empty spec.identityRef, but the name need not exist when you create it. The controllers check it at reconcile time and report IdentityNotFound, NamespaceNotAllowed or IdentityNotAllowed through conditions, and start no Job. See identityRef and the pages for TerraformCluster, TerraformMachine and TerraformMachinePool.

Lifecycle

  • Create. Create the Secret, then the identity. The creator must be able to get the Secret. Ready becomes True once the Secret exists. See Create the identity.
  • Reference. An object in an allowed namespace sets identityRef; the manager creates the mirror there. See Reference it.
  • Rotate. Edit the Secret’s data in place. The mirror in each allowed namespace is rewritten the next time an object that uses the identity reconciles, and within one --sync-period at the latest. A Job created after that gets the new values. A running Job keeps its old environment for its whole run, and its mounted files follow the mirror. To switch to another Secret, edit spec.secretRef, which reconciles every user at once and runs the get check again. See Rotate credentials.
  • Revoke. Remove a namespace from spec.allowedNamespaces. The manager starts no new Job there and deletes the mirror in that namespace. An object being deleted in a revoked namespace waits to destroy until the namespace is allowed again. See Revoke access.
  • Delete. Refused while the identity is in use or still mirrored, as described in Validation. Deleting it never deletes the source Secret. See Delete an identity.