# Secrets, CRDs, and RBAC

Mistakes in secret key names, custom resource schemas, or RBAC permission rules are notoriously easy to miss until a pod fails to start in the cluster. Instead of passing those objects to `scope.mkResource`, you build them with dedicated `scope` functions (`scope.mkSecret`, `scope.mkCRD`, `scope.rbac.*`) so Kix can verify key names, schema fields, and role bindings during compilation.

:::note[Helpful background]
- [Resources and `out`](/docs/reference/resources-and-out/): review how standard `scope` builders construct resources and expose `.out` attributes.
- [Manage secrets](/docs/how-to/platform-capabilities/use-kix-managed-secrets/): follow step-by-step instructions for wiring SOPS-encrypted or external Secrets into a package.
- [Using RBAC Authorization (Kubernetes docs)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/): review how Kubernetes `ServiceAccount`, `Role`, and `RoleBinding` objects grant API permissions.
- [Custom Resources (Kubernetes docs)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/): understand how Kubernetes operators extend the API with `CustomResourceDefinition` objects.
:::

## Secrets and secret refs

`scope.mkResource` refuses `v1/Secret` manifests directly. Instead, create secrets with `scope.mkSecret` (when Kix renders the `Secret` resource) or `scope.mkSecretRef` (when an operator or external controller populates the `Secret` in the cluster). All secrets defined in a package's `build` return set belong in the `secrets = { ... };` folder.

```nix
secrets.dbCredentials = scope.mkSecret {
  name = "myapp-db-credentials";
  source = ./secrets/db-credentials.sops.yaml;
  hash = "sha256:4f8b42c22dd3729b519ba6f68d2da7cc5b2d606d05daed5ad5128cc03e6c6358";
  keys = [ "username" "password" "host" ];
  stringData = {
    host = self.postgres.out.fqdn;
  };
  plainKeys = [ "host" ];
};
```

### `scope.mkSecret` arguments

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | string | Required | `metadata.name` of the `Secret`. |
| `keys` | list of strings | Auto-derived from `stringData`, `extra.data`, and `extra.stringData` | Declared secret key names. Required when `source` is set without inline keys or when `type = "kubernetes.io/service-account-token"`. |
| `type` | string | `"Opaque"` | Kubernetes Secret `type` (for example, `"Opaque"`, `"kubernetes.io/tls"`, or `"kubernetes.io/service-account-token"`). |
| `stringData` | attrset of strings | `{ }` | Inline plaintext key-value entries. Marks each key's provenance as `"inline"` in `kix.run/secret-sources` (unless listed in `plainKeys`), which is flagged by `security.secretSource` unless `meta.classification = "configuration"` or `"inline"` is accepted in `cluster.secrets.accept`. |
| `source` | path or string or `null` | `null` | Path to a SOPS-encrypted file decrypted at deploy time by `kix deploy`. Marks keys without inline values as `"sops"`. |
| `hash` | `"sha256:<64-hex>"` or `null` | `null` | Expected SHA-256 content hash of the encrypted `source` file. Required whenever `source` is a path. |
| `plainKeys` | list of strings | `[ ]` | Key names whose values are already set in `stringData`, `extra.data`, or `extra.stringData` and hold non-sensitive configuration (such as a username or hostname). Keys listed in `plainKeys` are recorded with provenance `"plain"` in `kix.run/secret-sources`; listing a key without a value throws an evaluation error. |
| `meta.classification` | `"configuration"` or `null` | `null` | Marks an inline `Secret` as non-credential configuration (used when an upstream application requires a `Secret` rather than a `ConfigMap` for its config file). |
| `meta.rotationPolicy` | `"manual"` \| `"automated"` \| `"ephemeral"` \| `"immutable"` or `null` | `null` | Documents how the secret is rotated (`kix.run/rotation-policy`). |
| `labels` | attrset | `{ }` | Extra `metadata.labels`. |
| `annotations` | attrset | `{ }` | Extra `metadata.annotations`. |
| `requires` | list of resources | `[ ]` | Explicit deployment dependencies. |
| `extra` | attrset | `{ }` | Extra top-level fields (`immutable = true`, `data`, `stringData`, etc.). Keys in `extra.data` and `extra.stringData` are counted as valued inline keys alongside `stringData`. |

Every key in `keys` must have either a value (in `stringData`, `extra.data`, or `extra.stringData`) or a SOPS `source`. A key with neither throws an evaluation error, except when `type == "kubernetes.io/service-account-token"` (where Kubernetes populates the token and Kix records `"controller"` in `kix.run/secret-sources`). `scope.mkSecret` never sets `kix.run/cede-fields`; for Secrets created and populated at runtime by an operator or external controller, use `scope.mkSecretRef`.

### `scope.mkSecretRef`

Use `scope.mkSecretRef` when an operator or external process creates the `Secret` object itself at runtime (for example, cert-manager creating a TLS `Secret` or CloudNativePG creating an application credentials `Secret`):

```nix
secrets.appUser = scope.mkSecretRef {
  name = "${self.pgCluster.out.name}-app";
  keys = [ "username" "password" "uri" ];
  requires = [ self.pgCluster ];
};
```

| `scope.mkSecretRef` argument | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | string | Required | Runtime `Secret` name in `scope.namespaceName`. |
| `keys` | list of strings | Required | Keys expected to exist on the runtime `Secret`. May be `[ ]` when workloads mount or import the whole Secret (`envFrom` or `volume`) without reading individual keys by name. |
| `type` | string | `"Opaque"` | Expected Secret type, recorded on the marker as `kix.run/secret-type`. |
| `requires` | list of resources | `[ ]` | Resources (such as the `Certificate` or database `Cluster` CR) that must be applied before the runtime `Secret` is checked. |
| `reflect` | bool | `false` | Marks the Secret reference as reflected into `scope.namespaceName`. |

`scope.mkSecretRef` emits a synthetic `PackageInstance` import marker (`secret-ref-<instance>-<name>`) carrying `kix.run/mode: "import"`, `kix.run/secret-type`, and (when `keys` is non-empty) `kix.run/import-secret-keys` so `kix deploy` verifies that both the `Secret` and all declared `keys` exist at deploy time.

### Secret `.out` consumption helpers

Both `scope.mkSecret` and `scope.mkSecretRef` return the same `.out` helpers. Every helper validates requested key names against the Secret's declared `keys` at Nix evaluation time and attaches the Secret's store-path context so the consuming workload automatically depends on the Secret:

| `.out` field or function | Signature | Returned value |
| --- | --- | --- |
| `out.name` | string | Secret `metadata.name` (with store context and namespace metadata). |
| `out.keys` | list of strings | Declared key names on the Secret. |
| `out.type` | string | Declared Secret `type`. |
| `out.keyRef` | `key -> attrset` | `{ valueFrom.secretKeyRef = { name = out.name; key = key; }; }`. Throws an evaluation error if `key` is not in `out.keys`. Ready to pass to `kix.mkEnvVars`. |
| `out.mkEnv` | `{ ENV_VAR = "key"; ... } -> list` | Maps environment variable names to Secret keys, validates every key against `out.keys`, and returns a sorted list of `{ name; valueFrom.secretKeyRef; }` entries. |
| `out.envFrom` | attrset | `{ secretRef.name = out.name; }` for container `envFrom` lists. |
| `out.volume` | `volName -> attrset` | `{ name = volName; secret.secretName = out.name; }` for pod `volumes` lists (or use [`kix.mount.secret`](/docs/reference/composition-helpers/#mount-helpers)). |

```nix
# Consuming a Secret via keyRef, mkEnv, and envFrom
env = kix.mkEnv {
  DB_HOST = self.postgres.out.fqdn;
  DB_PASS = self.secrets.dbCredentials.out.keyRef "password";
} (self.secrets.dbCredentials.out.mkEnv {
  DB_USER = "username";
});

envFrom = [ self.secrets.dbCredentials.out.envFrom ];
```

To validate keys on a Secret passed across packages as a `build` dependency, use [`kix.checkSecretKeys`](/docs/reference/package-anatomy/#kixchecksecretkeys).

---

## CRDs and CRD refs

Like Secrets, `CustomResourceDefinition` resources cannot be created with `scope.mkResource`. Use `scope.mkCRD` when your package renders the CRD manifest, or `scope.mkCRDRef` when the CRD is installed by an external component or operator at runtime. Place CRDs in the `crds = { ... };` folder of `build` so Kix exposes them on the dependency view at `dep.crds.<Kind>` (or on `root.out.crds.<Kind>` when using Helm `mkParts` or `kix.mkImport { crds = [ ... ]; }`, which is accessed at `dep.out.crds.<Kind>`).

### `scope.mkCRD` arguments

```nix
crds.ServiceMonitor = scope.mkCRD {
  spec = (builtins.fromJSON (builtins.readFile ./crds/servicemonitors.json)).spec;
  readiness = null;
  holdsData = false;
};
```

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `spec` | attrset | Required | Full `CustomResourceDefinition` `.spec` (`group`, `names.kind`, `names.plural`, `scope`, and `versions` with at least one `storage = true` and `served = true` version). `metadata.name` is derived automatically as `"${spec.names.plural}.${spec.group}"`. |
| `labels` | attrset | `{ }` | Extra `metadata.labels`. |
| `annotations` | attrset | `{ }` | Extra `metadata.annotations`. |
| `ownership.cede` | list of strings | `[ ]` | Field paths ceded to controllers (for example, `[ "spec.conversion.webhook.clientConfig.caBundle" ]`). |
| `readiness` | attrset or `null` | `null` | Default readiness check stamped on custom resources of this CRD (`{ condition = { type; status; }; }`, `{ path; equals; }`, or `{ path; notEquals; }`). It can also list `failed`, the states that mean a custom resource will not become ready until its spec changes: each entry is a condition (`{ condition = { type; status; reason ? }; }`) or a status field (`{ path; equals; }`), and may set `message` (the status fields to quote), `hint` (what to do next), and `rejected = true` (read a `path` entry as a rejected spec). `kix deploy` fails the resource as soon as the controller reports one. See [Declare readiness for custom resources](/docs/how-to/author-packages-and-clusters/declare-readiness-for-custom-resources/#declare-the-states-that-mean-failure). |
| `factBehavior` | `{ endpoint = "conduct" \| "terminate"; podSelector?; podNamespace?; selfScrapes?; }` or `null` | `null` | Default network-policy endpoint fact behavior for custom resources of this CRD. When non-null, `endpoint` is required, and `"terminate"` also requires `podSelector` (an attrset of labels or a function `cr: attrs`). |
| `usesAtLeast` | `(cr: usage)` or `null` | `null` | Function from a custom resource to the least it always uses, keyed like a ResourceQuota's `spec.hard` (`pods`, `requests.cpu`, `requests.storage`, and so on). Kix adds it to the namespace's ResourceQuota check. Return `{ }` when objects of this kind run no pods. See [Declaring quota usage](#declaring-quota-usage-usesatleast-and-kixusage). |
| `holdsData` | bool or `null` | `null` | Set to `true` when custom resources of this CRD manage stateful data (such as database clusters or object-store buckets), or `false` when they do not. Propagates `kix.run/holds-data` onto custom resources for deletion-protection checks. When `null`, no annotation is written (and stateful packages that create custom resources of this CRD fail build validation until `holdsData` is declared). |

### `scope.mkCRDRef` arguments

Use `scope.mkCRDRef` when a package creates custom resources for a CRD that is installed outside the package's own manifests (or dynamically by an operator):

```nix
crds.Certificate = scope.mkCRDRef {
  group = "cert-manager.io";
  kind = "Certificate";
  plural = "certificates";
  version = "v1";
  crdScope = "Namespaced";
  installedBy = deps.certManager.root;
  readiness = {
    condition = {
      type = "Ready";
      status = "True";
    };
  };
};
```

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `group` | string | Required | API group (for example, `"cert-manager.io"`). |
| `kind` | string | Required | PascalCase kind name (for example, `"Certificate"`). |
| `plural` | string | Required | Lowercase plural name (for example, `"certificates"`). |
| `version` | string | Required | Default API version used when building custom resources of this CRD (throws an evaluation error if omitted or `null`). |
| `servedVersions` | list of strings | `[ version ]` | All served API versions accepted by the CRD. |
| `crdScope` | `"Namespaced"` \| `"Cluster"` | `"Namespaced"` | Whether custom resources of this CRD are namespaced or cluster-scoped. |
| `installedBy` | resource, list of resources, or `null` | `null` | Operator workload or resource (or list of resources) that installs this CRD at runtime. Attaches its store context to the CRD ref marker so custom resources built with `scope.mkCR` wait for the installer. Every custom resource in a build must depend on a CRD, a CRD ref marker, or an `APIService` (enforced by [validation check 21](/docs/reference/validation-checks/#check-21-custom-resources-and-their-crd-declarations)). |
| `readiness` | attrset or `null` | `null` | Default readiness predicate stamped on custom resources of this CRD, in the same form as for `scope.mkCRD`, including `failed` entries with an optional `hint`. |
| `factBehavior` | `{ endpoint = "conduct" \| "terminate"; podSelector?; podNamespace?; selfScrapes?; }` or `null` | `null` | Default network-policy endpoint behavior for custom resources of this CRD (same requirements as on `scope.mkCRD`). |
| `usesAtLeast` | `(cr: usage)` or `null` | `null` | The least a custom resource always uses, as for `scope.mkCRD`. |
| `holdsData` | bool or `null` | `null` | Whether custom resources of this CRD hold stateful data (`true`, `false`, or `null` when undeclared). |

### Creating custom resources (`scope.mkCR`, `crd.out.mkCR`, and `crd.out.cr`)

Once a CRD is defined via `scope.mkCRD` or `scope.mkCRDRef` (or received from a dependency at `dep.crds.<Kind>` or `dep.out.crds.<Kind>`), construct custom resources using any of these three equivalent forms (`crd.mkCR` and `crd.cr` are also available directly on the handle):

1. **`scope.mkCR crd args`**: Builds the custom resource in `scope`, automatically setting `apiVersion` (`"${crd.group}/${crd.version}"` or `args.version`), `kind`, cluster vs. namespaced scoping, `readiness`, `factBehavior`, `holdsData`, `usesAtLeast`, and the deployment dependency on `crd`.
2. **`crd.out.mkCR consumerScope args`** (or `crd.mkCR consumerScope args`): Identical to `consumerScope.mkCR crd args`. Convenient when calling a CRD exported by a dependency (`prometheus.out.crds.ServiceMonitor.mkCR scope { name = "echo"; spec = { ... }; }` or `operator.crds.Widget.out.mkCR scope { name = "w"; spec = { ... }; }`).
3. **`scope.mkResource (crd.out.cr args)`** (or `crd.cr args`): Returns the attribute set for `scope.mkResource` (or `scope.mkClusterResource` when `crdScope == "Cluster"`).

### Declaring quota usage (`usesAtLeast` and `kix.usage`)

When a namespace has a ResourceQuota, Kix adds up the least its objects always use and refuses the build when that total is over a limit. Built-in workloads (Deployments, StatefulSets, Jobs, Pods) are counted from their pod templates. A custom resource is counted only when its kind declares `usesAtLeast`: a function from the custom resource to its usage, keyed like a ResourceQuota's `spec.hard`. Set it where the kind is declared: on `scope.mkCRD`, on `scope.mkCRDRef`, or in the `crds` entry of `kix.mkImport`.

Declare `usesAtLeast` on every kind the operator always turns into pods or claims, with `kix.usage`:

| Helper | Signature | Description |
| --- | --- | --- |
| `kix.usage.ofPod` | `containers or podSpec -> usage` | One pod's usage from its regular containers (`pods = 1`, plus `requests.*` and `limits.*`). A container's missing request counts as its limit, as in Kubernetes. |
| `kix.usage.times` | `count -> usage -> usage` | Multiplies every value by `count`. |
| `kix.usage.add` | `[ usage ] -> usage` | Adds a list of usages. Values may be integers or quantity strings (`"20Gi"`, `"500m"`). |

```nix
usesAtLeast =
  cr:
  kix.usage.times (cr.spec.instances or 1) (
    kix.usage.ofPod [ { resources = cr.spec.resources or { }; } ]
  );
```

Count only what the operator always runs and whose size the custom resource sets. A figure that is too high fails a valid build, so when unsure, leave a key out.

For a kind whose objects run no pods (a monitoring rule, a certificate, a policy), declare `usesAtLeast = _: { }`. Kix then knows the kind adds nothing and leaves it out of the refusal message. A declaration that returns `{ }` for any other reason, such as a value it reads being unset, also means "uses nothing", so return the real amount or leave the kind undeclared.

When a namespace is over a quota, the error lists what Kix counted and what it could not count, so you can see that the real usage may be higher:

```
namespace "app" exceeds ResourceQuota "app-quota" for pods (uses at least 2, limit is 1)
  Kubernetes will reject pods over this limit at deploy time.

  Counted (2 pods):
    Deployment/w: 2 pods

  Not counted (actual usage may be higher):
    DaemonSet/w: 1 pod per matching node (Kix does not know how many nodes the cluster has)
    35 PrometheusRule, 1 Widget: pod usage is not declared by their packages (most custom resources create no pods, but Kix cannot verify this)

  To fix this, raise the pods limit in "app-quota" or run fewer pods in namespace "app".
```

A custom kind under "Not counted" means its package declares no `usesAtLeast`. Add one to the kind's declaration to have Kix count it, or `_: { }` to drop it from the list.

### CRD spec construction helpers (`scope.crd.*`)

When authoring a CRD schema in Nix instead of loading a vendored JSON file, use the composable transformers in `scope.crd`, which build an attribute set you can pass to `scope.mkCRD`:

| Helper | Signature | Description |
| --- | --- | --- |
| `scope.crd.fromKind` | `kind -> extra -> crdAttrs` | Creates a base Namespaced CRD definition for PascalCase `kind` (`plural = "${lib.toLower kind}s"`, `singular = lib.toLower kind`, `listKind = "${kind}List"`, one served storage version `"v1"`, `subresources.status = { }`, and a base `openAPIV3Schema` with `spec` and `status`), deep-merged with `extra` (such as `{ spec.group = "example.com"; }`). |
| `scope.crd.clusterScoped` | `crdAttrs -> crdAttrs` | Sets `spec.scope = "Cluster"`. |
| `scope.crd.withVersion` | `version -> crdAttrs -> crdAttrs` | Sets `name = version` on every version entry in `spec.versions`. |
| `scope.crd.withShortNames` | `[ strings ] -> crdAttrs -> crdAttrs` | Sets `spec.names.shortNames`. |
| `scope.crd.withCategories` | `[ strings ] -> crdAttrs -> crdAttrs` | Sets `spec.names.categories`. |
| `scope.crd.withPrinterColumns` | `[ columns ] -> crdAttrs -> crdAttrs` | Sets `additionalPrinterColumns` on all versions in `spec.versions`. |
| `scope.crd.withSchema` | `specSchema -> crdAttrs -> crdAttrs` | Replaces `schema.openAPIV3Schema.properties.spec` on all versions in `spec.versions`. |
| `scope.crd.withLabels` | `labels -> crdAttrs -> crdAttrs` | Replaces top-level `labels` on the CRD definition. |
| `scope.crd.withAnnotations` | `annotations -> crdAttrs -> crdAttrs` | Merges `annotations` onto the CRD definition's top-level `annotations`. |

---

## RBAC helpers

### Creating a `ServiceAccount`

Create a `ServiceAccount` with `scope.mkResource` and pass it to a workload's `spec.template.spec.serviceAccount` shorthand:

```nix
serviceAccount = scope.mkResource {
  apiVersion = "v1";
  kind = "ServiceAccount";
  name = "my-operator";
};

deployment = scope.mkDeployment {
  name = "my-operator";
  spec = {
    selector.matchLabels = scope.selectorLabels;
    template.spec = {
      serviceAccount = self.serviceAccount; # Sets serviceAccountName and deployment dependency
      containers = [ ... ];
    };
  };
};
```

### Policy rule DSL (`scope.rbac.*`)

Inside a package, use `scope.rbac` (also exposed on the top-level cluster library as `kix.rbac`, though not on the package `kix` argument) to construct RBAC policy rules by API group and intent instead of writing raw `apiGroups`, `resources`, and `verbs` arrays by hand:

```nix
rules =
  let
    inherit (scope.rbac) core group restrictTo grant watch reconcile readWrite emit;
    certManager = group "cert-manager.io" [ "certificates" "issuers" ];
  in
  grant [
    # Watch and update status on Certificates and Issuers in cert-manager.io
    (reconcile [ certManager.certificates certManager.issuers ])

    # Read Secrets and ConfigMaps in the core ("") API group
    (watch [ core.secrets core.configmaps ])

    # Update a specific ConfigMap by resourceName
    (readWrite (restrictTo [ "my-operator-leader" ] core.configmaps))

    # Emit Kubernetes Events
    (emit core.events)
  ];
```

#### Target constructors and modifiers

| Function or value | Signature | Description |
| --- | --- | --- |
| `scope.rbac.group` | `apiGroup -> [ resources ] -> attrset` | Returns an attribute set mapping each resource name in `resources` to a target ref (`{ apiGroup; resource; resourceNames = null; }`). Also defines `__functor`, so the returned set can be called as a function `grp "otherresource"` for ad-hoc resource names in `apiGroup`. |
| `scope.rbac.core` | attrset (callable as ` string -> ref`) | Pre-built group for the core `""` API group with attributes `configmaps`, `endpoints`, `events`, `namespaces`, `nodes`, `persistentvolumeclaims`, `persistentvolumes`, `pods`, `secrets`, `serviceaccounts`, and `services` (plus `__functor` for other core resources, such as `core "replicationcontrollers"`). |
| `scope.rbac.sub` | `suffix -> ref -> ref` | Returns a target ref for the subresource `"${ref.resource}/${suffix}"` (where `suffix` is a single string such as `"status"` or `"finalizers"`, with `resourceNames = null`). |
| `scope.rbac.restrictTo` | `[ resourceNames ] -> ref -> ref` | Restricts a single target `ref` to the listed `resourceNames`. |

#### Intent constructors and rule normalizer

Each intent function accepts either a single target `ref` or a list of target `ref`s and returns a **list** of policy rule attribute sets. Pass a list of intent results to `scope.rbac.grant` to flatten and merge rules that share the same `apiGroups`, `verbs`, and `resourceNames`:

| Function | Signature / Verbs granted | Subresources added |
| --- | --- | --- |
| `scope.rbac.watch` | `[ "get" "list" "watch" ]` | None |
| `scope.rbac.reconcile` | `[ "get" "list" "watch" ]` on `ref`, `[ "patch" "update" ]` on `ref`, and `[ "patch" "update" ]` on `sub "status" ref` | `status` |
| `scope.rbac.finalize` | `[ "update" ]` on `sub "finalizers" ref` only | `finalizers` (only) |
| `scope.rbac.watchAndFinalize` | `[ "get" "list" "watch" ]` on `ref`, plus `[ "update" ]` on `sub "finalizers" ref` | `finalizers` |
| `scope.rbac.own` | `[ "get" "list" "watch" "create" "delete" ]` | None |
| `scope.rbac.readWrite` | `[ "get" "list" "watch" "create" "update" "delete" ]` | None |
| `scope.rbac.full` | `[ "get" "list" "watch" "create" "update" "delete" "patch" ]` | None |
| `scope.rbac.emit` | `[ "create" "patch" ]` | None (designed for `core.events`) |
| `scope.rbac.allow` | `[ verbs ] -> refOrRefs -> [ rules ]` | Custom verb list |
| `scope.rbac.grant` | `[ intentLists ] -> [ rules ]` | Flattens a list of intent rule lists and merges rules with identical `(apiGroups, verbs, resourceNames)`. |

### Role and RoleBinding set builders (`mkRoleSet` and `mkClusterRoleSet`)

`scope.rbac.mkRoleSet` and `scope.rbac.mkClusterRoleSet` generate paired `Role` + `RoleBinding` (or `ClusterRole` + `ClusterRoleBinding`) parts in a single call:

```nix
rbac =
  let
    inherit (scope.rbac) core group grant reconcile watch;
    widgets = group "example.com" [ "widgets" ];
  in
  scope.rbac.mkClusterRoleSet scope {
    subjects = [
      {
        kind = "ServiceAccount";
        name = self.serviceAccount.out.name;
        namespace = scope.namespaceName;
      }
    ];
    roles = {
      "my-operator-controller" = grant [
        (reconcile widgets.widgets)
        (watch [ core.pods core.services ])
      ];
    };
  };
```

| Builder | Signature | Returned parts attrset |
| --- | --- | --- |
| `scope.rbac.mkRoleSet` | `scope -> { subjects, roles, labels ? { } }` | For each `<roleName>` in `roles`, returns `<roleName>` (`Role`) and `"${roleName}-binding"` (`RoleBinding`). |
| `scope.rbac.mkClusterRoleSet` | `scope -> { subjects, roles, labels ? { } }` | For each `<roleName>` in `roles`, returns `<roleName>` (`ClusterRole`) and `"${roleName}-binding"` (`ClusterRoleBinding`). |

Each entry in `roles` can be a list of policy rules (`roles.<roleName> = [ ... ]`) or `{ rules = [ ... ]; bindingName ? roleName; }`. The `subjects` list is passed through unchanged to the binding's `subjects` field, so each entry must be a Kubernetes subject attribute set (`{ kind = "ServiceAccount"; name = self.serviceAccount.out.name; namespace = scope.namespaceName; }`). Referencing `self.serviceAccount.out.name` attaches its Nix string context so the binding automatically depends on the `ServiceAccount`.