Skip to content
Install the CLI

Reference Package authoring

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.

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.

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" ];
};
ArgumentTypeDefaultDescription
namestringRequiredmetadata.name of the Secret.
keyslist of stringsAuto-derived from stringData, extra.data, and extra.stringDataDeclared secret key names. Required when source is set without inline keys or when type = "kubernetes.io/service-account-token".
typestring"Opaque"Kubernetes Secret type (for example, "Opaque", "kubernetes.io/tls", or "kubernetes.io/service-account-token").
stringDataattrset 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.
sourcepath or string or nullnullPath to a SOPS-encrypted file decrypted at deploy time by kix deploy. Marks keys without inline values as "sops".
hash"sha256:<64-hex>" or nullnullExpected SHA-256 content hash of the encrypted source file. Required whenever source is a path.
plainKeyslist 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 nullnullMarks 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 nullnullDocuments how the secret is rotated (kix.run/rotation-policy).
labelsattrset{ }Extra metadata.labels.
annotationsattrset{ }Extra metadata.annotations.
requireslist of resources[ ]Explicit deployment dependencies.
extraattrset{ }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.

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):

secrets.appUser = scope.mkSecretRef {
name = "${self.pgCluster.out.name}-app";
keys = [ "username" "password" "uri" ];
requires = [ self.pgCluster ];
};
scope.mkSecretRef argumentTypeDefaultDescription
namestringRequiredRuntime Secret name in scope.namespaceName.
keyslist of stringsRequiredKeys 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.
typestring"Opaque"Expected Secret type, recorded on the marker as kix.run/secret-type.
requireslist of resources[ ]Resources (such as the Certificate or database Cluster CR) that must be applied before the runtime Secret is checked.
reflectboolfalseMarks 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.

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 functionSignatureReturned value
out.namestringSecret metadata.name (with store context and namespace metadata).
out.keyslist of stringsDeclared key names on the Secret.
out.typestringDeclared Secret type.
out.keyRefkey -> 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"; ... } -> listMaps environment variable names to Secret keys, validates every key against out.keys, and returns a sorted list of { name; valueFrom.secretKeyRef; } entries.
out.envFromattrset{ secretRef.name = out.name; } for container envFrom lists.
out.volumevolName -> attrset{ name = volName; secret.secretName = out.name; } for pod volumes lists (or use kix.mount.secret).
# 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.


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>).

crds.ServiceMonitor = scope.mkCRD {
spec = (builtins.fromJSON (builtins.readFile ./crds/servicemonitors.json)).spec;
readiness = null;
holdsData = false;
};
ArgumentTypeDefaultDescription
specattrsetRequiredFull 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}".
labelsattrset{ }Extra metadata.labels.
annotationsattrset{ }Extra metadata.annotations.
ownership.cedelist of strings[ ]Field paths ceded to controllers (for example, [ "spec.conversion.webhook.clientConfig.caBundle" ]).
readinessattrset or nullnullDefault 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.
factBehavior{ endpoint = "conduct" | "terminate"; podSelector?; podNamespace?; selfScrapes?; } or nullnullDefault 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 nullnullFunction 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.
holdsDatabool or nullnullSet 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).

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):

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";
};
};
};
ArgumentTypeDefaultDescription
groupstringRequiredAPI group (for example, "cert-manager.io").
kindstringRequiredPascalCase kind name (for example, "Certificate").
pluralstringRequiredLowercase plural name (for example, "certificates").
versionstringRequiredDefault API version used when building custom resources of this CRD (throws an evaluation error if omitted or null).
servedVersionslist 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.
installedByresource, list of resources, or nullnullOperator 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).
readinessattrset or nullnullDefault 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 nullnullDefault network-policy endpoint behavior for custom resources of this CRD (same requirements as on scope.mkCRD).
usesAtLeast(cr: usage) or nullnullThe least a custom resource always uses, as for scope.mkCRD.
holdsDatabool or nullnullWhether 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)

Section titled “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)

Section titled “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:

HelperSignatureDescription
kix.usage.ofPodcontainers or podSpec -> usageOne 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.timescount -> usage -> usageMultiplies every value by count.
kix.usage.add[ usage ] -> usageAdds a list of usages. Values may be integers or quantity strings ("20Gi", "500m").
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.*)

Section titled “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:

HelperSignatureDescription
scope.crd.fromKindkind -> extra -> crdAttrsCreates 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.clusterScopedcrdAttrs -> crdAttrsSets spec.scope = "Cluster".
scope.crd.withVersionversion -> crdAttrs -> crdAttrsSets name = version on every version entry in spec.versions.
scope.crd.withShortNames[ strings ] -> crdAttrs -> crdAttrsSets spec.names.shortNames.
scope.crd.withCategories[ strings ] -> crdAttrs -> crdAttrsSets spec.names.categories.
scope.crd.withPrinterColumns[ columns ] -> crdAttrs -> crdAttrsSets additionalPrinterColumns on all versions in spec.versions.
scope.crd.withSchemaspecSchema -> crdAttrs -> crdAttrsReplaces schema.openAPIV3Schema.properties.spec on all versions in spec.versions.
scope.crd.withLabelslabels -> crdAttrs -> crdAttrsReplaces top-level labels on the CRD definition.
scope.crd.withAnnotationsannotations -> crdAttrs -> crdAttrsMerges annotations onto the CRD definition’s top-level annotations.

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

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 = [ ... ];
};
};
};

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:

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)
];
Function or valueSignatureDescription
scope.rbac.groupapiGroup -> [ resources ] -> attrsetReturns 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.coreattrset (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.subsuffix -> ref -> refReturns 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 -> refRestricts a single target ref to the listed resourceNames.

Each intent function accepts either a single target ref or a list of target refs 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:

FunctionSignature / Verbs grantedSubresources 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" refstatus
scope.rbac.finalize[ "update" ] on sub "finalizers" ref onlyfinalizers (only)
scope.rbac.watchAndFinalize[ "get" "list" "watch" ] on ref, plus [ "update" ] on sub "finalizers" reffinalizers
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)

Section titled “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:

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 ])
];
};
};
BuilderSignatureReturned parts attrset
scope.rbac.mkRoleSetscope -> { subjects, roles, labels ? { } }For each <roleName> in roles, returns <roleName> (Role) and "${roleName}-binding" (RoleBinding).
scope.rbac.mkClusterRoleSetscope -> { 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.