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:
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.
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"
- The Secret’s name. A DNS subdomain, up to 253 characters.
- The Secret’s namespace. Required, and any namespace works. It does not have to be an allowed namespace.
- Up to 100 namespace names. At least one if
listis set. - 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.
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 withTF_orKUBE_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 mode0440. Every key appears as a file, includingTF_andKUBE_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
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:
specmust have at least one property, andspec.secretRef.nameandspec.secretRef.namespaceare required, with the length and character limits in Secret reference.spec.allowedNamespacesmust setlist,selectoror both. The empty object is rejected with a message telling you to writeselector: {}.spec.allowedNamespaces.listhas 1 to 100 unique items, each a DNS label.status.conditionshas at most 32 items andstatus.namespacesat most 1000.
Webhook rules, on create and update:
spec.secretRef.nameandspec.secretRef.namespacemust be non-empty.- Each
spec.allowedNamespaces.listitem must be a valid DNS label. spec.allowedNamespaces.selectormust parse as a label selector.- The requesting user must be allowed to
getthe Secret. The webhook sends aSubjectAccessReviewfor verbgetonsecretsinspec.secretRef.namespace, namedspec.secretRef.name, as the requester (user, groups and extra attributes). If it is denied, the request fails withuser "<name>" may not get Secret <namespace>/<name>. On create the check always runs. On update it runs only whenspec.secretReforspec.allowedNamespaceschanged, 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,TerraformMachineorTerraformMachinePool, in any namespace, resolves to the identity through its ownidentityRefor a fallback. An object being deleted still counts. The message names one such object. - The delete is refused while
status.namespacesis 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
getthe Secret.ReadybecomesTrueonce 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-periodat 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, editspec.secretRef, which reconciles every user at once and runs thegetcheck 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.