Crossplane’s RBAC controls who can orchestrate cloud infrastructure by managing permissions on Crossplane’s Custom Resources (CRs) and their underlying infrastructure.

Let’s imagine we have a team that needs to provision PostgreSQL instances on AWS. We want to give them the ability to create RDSInstance resources, but only in a specific region and with specific instance classes. We also want to prevent them from deleting instances they didn’t create.

Here’s how we can set that up using Kubernetes RBAC and Crossplane’s CompositeResourceDefinition (XRD) and CompositeResource (Composite) concepts.

First, let’s define a CompositeResourceDefinition (XRD) that models our desired PostgreSQL instance. This XRD will be the blueprint for what our users can provision.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: rdsinstances.rds.example.org
spec:
  group: rds.example.org
  names:
    kind: RDSInstance
    plural: rdsinstances
  claimNames:
    kind: RDSClaim
    plural: rdsclaims
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  storageGB:
                    type: integer
                    description: "Storage in GB"
                  instanceClass:
                    type: string
                    description: "AWS RDS instance class"
                required:
                - storageGB
                - instanceClass
            required:
            - parameters

Next, we’ll create a CompositeResource (Composite) that uses this XRD to define the actual structure of what can be provisioned. This Composite will tell Crossplane how to translate the user’s RDSClaim into a specific RDSInstance.

apiVersion: rds.example.org/v1alpha1
kind: RDSInstance
metadata:
  name: managed-postgres-template
spec:
  parameters:
    storageGB: 20
    instanceClass: db.t3.small

Now, we need to create a CompositeResourceClaim (Claim) that our team will use to request a PostgreSQL instance. This is the resource they will interact with directly.

apiVersion: rds.example.org/v1alpha1
kind: RDSClaim
metadata:
  name: my-team-postgres
  namespace: team-a
spec:
  parameters:
    storageGB: 30
    instanceClass: db.r5.large

With these resources in place, let’s talk about RBAC. Crossplane leverages Kubernetes RBAC to control access. The key is understanding which resources Crossplane creates and how to grant permissions on them.

When a RDSClaim is created in the team-a namespace, Crossplane’s Kubernetes controller (specifically the KubernetesRuntime or KubernetesPlatform if you’re using those) will typically create a CompositeResource (e.g., RDSInstance) and then provision the underlying cloud provider resource (e.g., an AWS RDS instance).

The most common way to control who can provision what is by granting permissions on the CompositeResource kinds and their corresponding CompositeResourceClaim kinds within specific namespaces.

Cause 1: Users can’t create claims.

  • Diagnosis: Check if the user has create permission on rdsclaims.rds.example.org in their target namespace.
    kubectl auth can-i create rdsclaims.rds.example.org --namespace team-a
    
  • Fix: Create a Role or ClusterRole that grants this permission.
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: rds-provisioner-role
      namespace: team-a
    rules:
    - apiGroups: ["rds.example.org"]
      resources: ["rdsclaims"]
      verbs: ["create", "get", "list", "watch"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: rds-provisioner-binding
      namespace: team-a
    subjects:
    - kind: User
      name: team-a-developer@example.com # Or a ServiceAccount
      apiGroup: rbac.authorization.k8s.io
    roleRef:
      kind: Role
      name: rds-provisioner-role
      apiGroup: rbac.authorization.k8s.io
    
  • Why it works: This grants the user the ability to request the creation of an RDS instance via the RDSClaim object.

Cause 2: Users can create claims but can’t see the underlying composite resource.

  • Diagnosis: The CompositeResource (e.g., RDSInstance) is created by Crossplane, often in a different namespace (or the same one, depending on configuration). Users need to get, list, and watch these.
    kubectl auth can-i get rdsinstances.rds.example.org --namespace team-a
    
  • Fix: Grant get, list, watch permissions on the CompositeResource kind.
    # Add to the existing Role or create a new one
    - apiGroups: ["rds.example.org"]
      resources: ["rdsinstances"]
      verbs: ["get", "list", "watch"]
    
  • Why it works: This allows users to view the status and details of the CompositeResource that represents their provisioned infrastructure, which is crucial for debugging and monitoring.

Cause 3: Users can create claims but can’t see the actual cloud provider resource (e.g., AWS RDS instance).

  • Diagnosis: Crossplane provisions actual cloud resources. The RBAC for these resources is managed by the cloud provider’s IAM system, not Kubernetes RBAC. However, Crossplane’s ability to manage these resources is controlled by the ProviderConfig and the permissions of the service account running the Crossplane controller. If a user wants to see the Kubernetes representation of the managed resource (e.g., the RDSInstance in our example), they need permissions on that.
  • Fix: Ensure the CompositeResource (e.g., RDSInstance) is visible to the user. This is usually covered by Cause 2, as the RDSInstance is the Kubernetes object representing the cloud resource. To see the actual cloud resource, they’d need IAM permissions in AWS.
  • Why it works: The RDSInstance object in Kubernetes mirrors the state of the actual AWS RDS instance. RBAC on the RDSInstance controls visibility into this mirrored state.

Cause 4: Users can delete claims they didn’t create.

  • Diagnosis: The default delete permission on rdsclaims might be too broad.
    kubectl auth can-i delete rdsclaims.rds.example.org --namespace team-a
    
  • Fix: Restrict delete permissions to only the specific claims a user or service account owns. This is achieved by using ResourceInstance selectors in RoleBinding or by creating Role objects scoped to individual RDSClaim resources (though this is less common and harder to manage at scale). A more practical approach is to limit who can delete by namespace and potentially by label selectors if you have a convention.
    # Example: Limit delete to specific labels if you have a naming convention
    # This is a simplified example; true ownership control is complex.
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: rds-delete-limiter
      namespace: team-a
    rules:
    - apiGroups: ["rds.example.org"]
      resources: ["rdsclaims"]
      verbs: ["get", "list", "watch"] # Allow viewing
    - apiGroups: ["rds.example.org"]
      resources: ["rdsclaims"]
      verbs: ["delete"]
      resourceNames: ["my-team-postgres"] # Only allow deleting this specific one
    
    Note: resourceNames is powerful but requires explicit management for each resource. Often, you’d grant delete only to cluster admins or a dedicated operations team.
  • Why it works: By restricting delete to specific resourceNames or by carefully managing namespace-level delete permissions, you ensure that users can only remove the infrastructure they are authorized to manage.

Cause 5: Users can’t modify specific parameters of their claims.

  • Diagnosis: The CompositeResourceDefinition’s schema and the CompositeResource’s structure dictate what can be changed. RBAC can limit which fields can be updated.
    # This is conceptually checked by admission controllers, not directly by `kubectl auth can-i`
    # You'd see a validation error during `kubectl apply` or `kubectl edit`.
    
  • Fix: Use Kubernetes Admission Controllers (like ValidatingAdmissionWebhook) to enforce policies. Crossplane’s CompositeResourceDefinition schema also enforces basic type and required field validation. For finer-grained control over who can change what parameter, you’d typically use an admission controller that inspects the spec.parameters of the RDSClaim.
    # Example of a policy that might be enforced by an admission controller
    # This is not a direct RBAC rule but a policy it enables.
    # The admission controller would reject changes to instanceClass if not allowed.
    
  • Why it works: Admission controllers intercept requests to the Kubernetes API server before they are persisted. They can validate the request payload against custom rules, preventing unauthorized modifications to specific fields within a resource.

Cause 6: Users can’t provision resources outside of specific namespaces or with disallowed configurations.

  • Diagnosis: Check if the RoleBinding is correctly scoped to the namespace where the user is trying to provision. Also, check the CompositeResourceDefinition for schema constraints.
  • Fix: Ensure RoleBinding objects are namespace-scoped correctly. For disallowed configurations (e.g., instance types not in a predefined list), use ValidatingAdmissionWebhooks or leverage the schema validation within the CompositeResourceDefinition itself to reject invalid spec.parameters.
    # Example: Limiting instanceClass via XRD schema
    # ... inside RDSInstance spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.parameters.properties.instanceClass
    enum: ["db.t3.small", "db.r5.large"]
    
  • Why it works: Namespace-scoped RBAC restricts actions to specific namespaces. Schema validation in the XRD prevents the creation of CompositeResources that don’t conform to defined rules, including valid parameter values.

The next thing you’ll likely run into is managing the lifecycle of these resources, specifically how to handle updates and deletions gracefully, which often involves understanding Crossplane’s Composition and ResourceSelector configurations.

Want structured learning?

Take the full Crossplane course →