Skip to content
Install the CLI

Tutorial 12

Scorecards and compile-time policy

In tutorial 09, you used lib.mkOption and lib.types to validate each option on a single package instance. Some policies, however, belong to the platform team rather than to an individual package author:

  • “Every reverse-proxy gateway in our production cluster must set maxBodySize explicitly rather than relying on the 1m default.”
  • “No two reverse-proxy gateways across the entire cluster may claim the same proxyPath.”

In Kix, you enforce these rules at build time using scorecards. Because scorecards evaluate inside Nix against the typed package options, metadata, and full cluster graph, they can check cross-instance invariants that single-manifest YAML linters cannot see.

Open:

tutorials/19-scorecards/cluster.nix
tutorials/19-scorecards/scorecard.nix

In tutorials/19-scorecards/cluster.nix, the cluster defines two application namespaces (app-v1 and app-v2), each with a backend and a gateway instance:

  • app-v1.gateway sets proxyPath = "/v1/" and maxBodySize = "10m".
  • app-v2.gateway sets proxyPath = "/v2/" and maxBodySize = "10m".

At the top of cluster.nix, the cluster module inside modules sets scorecard by importing ./scorecard.nix:

tutorials/19-scorecards/cluster.nix
kix.buildCluster {
name = "19-scorecards";
modules = [
kix.flavors.kind
# `lib` is injected by the NixOS module system — we pull it in
# here so we can hand it to ./scorecard.nix on import.
(
{ ref, lib, ... }:
{
# ── Scorecard ────────────────────────────────────────────────
# The whole rules block lives in ./scorecard.nix. cluster.nix
# stays focused on what the cluster *contains*; the rules file
# owns the checks we want to run against it.
scorecard = import ./scorecard.nix { inherit lib kix; };
# ...
}
)
];
}

View source on GitHub ↗

Keeping scorecard policy in a separate file (./scorecard.nix) lets platform teams share a single policy definition across multiple clusters while keeping cluster.nix focused on topology and instances.

Enable Built-In Rules And Severity Controls

Section titled “Enable Built-In Rules And Severity Controls”

Open tutorials/19-scorecards/scorecard.nix. At the top of the file, enable, ruleOverrides, and rules configure what the scorecard checks:

tutorials/19-scorecards/scorecard.nix
{
enable = true;
# Per-rule severity override. Key is `<category>.<rule>`.
# Raises a built-in from "warning" to "error" so the build fails
# if a probe is missing.
ruleOverrides.byRule."reliability.hasProbes".severity = "error";
rules = {
# 1. Whole built-in category. Bundles hasProbes,
# boundedResources, hasResourceLimits, hasResourceRequests,
# gracefulShutdown, hasPDB.
reliability = kix.rules.reliability;
# custom rules ...
};
}

View source on GitHub ↗

Here is what each field controls:

  • enable = true: Turns on scorecard evaluation during kix check and kix build.
  • ruleOverrides.byRule."reliability.hasProbes".severity = "error": Overrides the severity of a single rule (<category>.<rule>) from "warning" to "error".
  • rules = { reliability = kix.rules.reliability; ... }: Selects Kix’s built-in reliability rule category (kix.rules.reliability, alongside kix.rules.security, kix.rules.governance, and kix.rules.architecture) and adds custom rules for this cluster.
  • maxSeverity (optional, defaults to "warning"): Caps the effective severity of every finding in the cluster to "info", "warning", or "error". Because the default is "warning", findings from error-severity rules are capped to warnings unless the cluster also sets maxSeverity = "error".

Next, look at the first custom rule in tutorials/19-scorecards/scorecard.nix:

tutorials/19-scorecards/scorecard.nix
custom.proxyMaxBodySizeMustBeExplicit = {
description = "Reverse-proxy gateways must set maxBodySize explicitly (not the 1m default).";
level = "package";
severity = "error";
tags = [ "reliability" "kix-examples" ];
# Only run on reverse-proxy packages. `appliesTo` receives the
# package metadata directly, so this checks `meta.provides`.
appliesTo =
{ meta, ... }:
builtins.elem "reverse-proxy" (meta.provides or [ ]);
check =
{ config, instanceName, ... }:
if (config.maxBodySize or "") == "1m" then
[
{
message =
"instance '${instanceName}' uses the default maxBodySize '1m'. "
+ "nginx rejects larger request bodies — set this explicitly.";
}
]
else
[ ];
};

View source on GitHub ↗

A level = "package" rule runs once per package instance and inspects the package’s meta, config, and built resources:

  1. appliesTo = { meta, ... }: ...: Filters which instances this rule checks. Because reverse-proxy-package.nix declares meta.provides = [ "reverse-proxy" ], builtins.elem "reverse-proxy" (meta.provides or [ ]) runs this rule only on reverse-proxy instances and skips echo-server backends.
  2. check = { config, instanceName, ... }: ...: Inspects the instance’s merged config directly. You do not have to parse nginx’s default.conf text out of a rendered ConfigMap; you read config.maxBodySize as a structured Nix value. If it equals "1m", check returns a finding with a descriptive message.

Some mistakes only appear when you compare multiple instances across the cluster. If app-v1/gateway and app-v2/gateway both configure proxyPath = "/v1/", each instance looks completely valid in isolation, yet their routes collide across the cluster.

To catch that, tutorials/19-scorecards/scorecard.nix defines a helper predicate isProxy and a level = "cluster" rule named custom.noProxyPathCollision:

tutorials/19-scorecards/scorecard.nix
isProxy =
inst:
# Cluster-level rules inspect built instances, so the package
# metadata is under `inst._meta.packageMeta`.
builtins.elem "reverse-proxy" (inst._meta.packageMeta.provides or [ ]);

View source on GitHub ↗

tutorials/19-scorecards/scorecard.nix
custom.noProxyPathCollision = {
description = "No two reverse-proxy gateways may share the same proxyPath.";
level = "cluster";
severity = "error";
tags = [
"routing"
"kix-examples"
];
check =
{ allInstances, ... }:
let
# `allInstances` is `{ <ns> = { <name> = instance; }; }`.
# Flatten into a list of { ns, name, inst } records.
#
# `lib.concatLists` is the nixpkgs equivalent of JS's
# `arr.flat()` — flatten a list of lists into one list.
# `builtins.attrNames` returns the keys of an attrset.
flat = lib.concatLists (
map (
ns:
map (name: {
inherit ns name;
inst = allInstances.${ns}.${name};
}) (builtins.attrNames allInstances.${ns})
) (builtins.attrNames allInstances)
);
# Keep only reverse-proxy instances.
proxies = builtins.filter (e: isProxy e.inst) flat;
# Group by proxyPath → { "/v1/" = [...]; "/v2/" = [...]; }.
byPath = builtins.foldl' (
acc: e:
let
p = e.inst._meta.config.proxyPath or "?";
in
acc // { ${p} = (acc.${p} or [ ]) ++ [ e ]; }
) { } proxies;
# Any path with more than one owner is a collision.
collisions = lib.filterAttrs (_: members: builtins.length members > 1) byPath;
in
map (path: {
message =
"${toString (builtins.length collisions.${path})} reverse-proxy gateways share proxyPath '${path}': "
+ builtins.concatStringsSep ", " (
map (e: "${e.ns}/${e.name}") collisions.${path}
);
}) (builtins.attrNames collisions);
};

View source on GitHub ↗

Look at how custom.noProxyPathCollision works:

  1. level = "cluster": Runs once for the entire cluster and receives allInstances ({ <namespace> = { <instance> = inst; }; }).
  2. Filter to reverse proxies: Flattens allInstances and uses isProxy to keep only instances whose meta.provides includes "reverse-proxy".
  3. Group by config.proxyPath: Reads e.inst._meta.config.proxyPath on every reverse-proxy instance and groups instances by path.
  4. Report collisions: Any proxyPath claimed by more than one gateway produces an error finding that lists every colliding <namespace>/<instance> pair.

Run kix check 19-scorecards:

kix-examples/
❱ kix check 19-scorecards
 TOOL       RESULT  DETAILS                      
 eval       pass    24 manifests evaluated       
 scorecard  pass    0 errors, 6 warnings, 2 info

Both custom rules pass (0 errors), while the built-in kix.rules.reliability rules report 6 warnings and 2 informational findings across the 24 evaluated manifests. kix build 19-scorecards succeeds and renders all 24 resources:

kix-examples/ offline capture
❱ kix build 19-scorecards
Show outputHide output · 717 lines
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    kix.run/identity-hash: gvrlspcfgh9a2qnsx5kqxw9f4l3x52jj
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
  name: activations.kix.run
spec:
  group: kix.run
  names:
    kind: Activation
    listKind: ActivationList
    plural: activations
    singular: activation
  scope: Cluster
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            type: object
            x-kubernetes-preserve-unknown-fields: true
          status:
            type: object
            x-kubernetes-preserve-unknown-fields: true
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    kix.run/identity-hash: pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
  name: packageinstances.kix.run
spec:
  group: kix.run
  names:
    kind: PackageInstance
    listKind: PackageInstanceList
    plural: packageinstances
    singular: packageinstance
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            type: object
            x-kubernetes-preserve-unknown-fields: true
          status:
            type: object
            x-kubernetes-preserve-unknown-fields: true
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 5dj5rd7lw2dk1pv9pcnf6dpi46i93z47,95gmkzkbdf1k30pp3k54hp8vc8qd54p8
    kix.run/identity-hash: 9ph1i0ympz81gpl8xxvdriykp6q5c6lq
    kix.run/package: backend
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: backend
  name: backend
  namespace: app-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: backend
      app.kubernetes.io/name: backend
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: backend
        app.kubernetes.io/name: backend
    spec:
      containers:
      - image: docker.io/nginxinc/nginx-unprivileged:1.30.5-alpine@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 10
        name: nginx
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        readinessProbe:
          failureThreshold: 1
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 5
        resources:
          limits:
            cpu: 100m
            memory: 64Mi
          requests:
            cpu: 50m
            memory: 32Mi
        securityContext:
          readOnlyRootFilesystem: true
          runAsNonRoot: true
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
        - mountPath: /tmp
          name: tmp
      terminationGracePeriodSeconds: 30
      volumes:
      - configMap:
          name: backend
        name: config
      - emptyDir: {}
        name: tmp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,p1v3s0pi2ksrbbpw8mmdxnb57clwmzl4,requires:jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
    kix.run/identity-hash: yc2asr6hm9fawjk7f5kcmc1r7sq83wrx
    kix.run/package: gateway
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: gateway
  name: gateway
  namespace: app-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: gateway
      app.kubernetes.io/name: gateway
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: gateway
        app.kubernetes.io/name: gateway
    spec:
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 80
          periodSeconds: 10
        name: nginx
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /healthz
            port: 80
          periodSeconds: 5
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
      volumes:
      - configMap:
          name: gateway
        name: config
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 3az3y9dajri5bycnprn4kc14y884dypp,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/identity-hash: 7mlywnw2vzddhhag58w05y8sbk9p6pms
    kix.run/package: backend
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: backend
  name: backend
  namespace: app-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: backend
      app.kubernetes.io/name: backend
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: backend
        app.kubernetes.io/name: backend
    spec:
      containers:
      - image: docker.io/nginxinc/nginx-unprivileged:1.30.5-alpine@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 10
        name: nginx
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        readinessProbe:
          failureThreshold: 1
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 5
        resources:
          limits:
            cpu: 100m
            memory: 64Mi
          requests:
            cpu: 50m
            memory: 32Mi
        securityContext:
          readOnlyRootFilesystem: true
          runAsNonRoot: true
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
        - mountPath: /tmp
          name: tmp
      terminationGracePeriodSeconds: 30
      volumes:
      - configMap:
          name: backend
        name: config
      - emptyDir: {}
        name: tmp
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    kix.run/depends-on: 1bw942wwr3a5jy0h5mgv7zrmi97lhpm6,lxi0gciwfx6mgp92319mjxhjwvgkdqhs,requires:jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
    kix.run/identity-hash: 84760c477szfqhg7ihq3ni5a1x1a7z9r
    kix.run/package: gateway
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
    app.kubernetes.io/name: gateway
  name: gateway
  namespace: app-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/instance: gateway
      app.kubernetes.io/name: gateway
  template:
    metadata:
      labels:
        app.kubernetes.io/instance: gateway
        app.kubernetes.io/name: gateway
    spec:
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 80
          periodSeconds: 10
        name: nginx
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /healthz
            port: 80
          periodSeconds: 5
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: config
          readOnly: true
      volumes:
      - configMap:
          name: gateway
        name: config
---
apiVersion: kix.run/v1alpha1
kind: Activation
metadata:
  annotations:
    kix.run/built-via: /nix/store/hbvagl9f7fi6d8z50yp5v29jkimcl0xy-kix-activation-19-scorecards.json
    kix.run/depends-on: 35cl4xdxp4ivq0y758j4d7ks65i08dm5,h87xkj4fjz6ngyi6vq4qpdqwdf5rcxgk,jh345cnnffy1bnj4cz5x0hwq6ibxvkkr,ksz9y8ygfpc5bzbpxq1kz9pn4ahqjgxb,pq911a8cdinlzlhd4aaw2fnqz0brr1hr,r5sgwjk8w3m41l0xaj0s16xxsacd0nk6,requires:gvrlspcfgh9a2qnsx5kqxw9f4l3x52jj
    kix.run/identity-hash: v5f4pbxglm62hzcpz74p49vsbcp0dxdm
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kix.run/cluster: 19-scorecards
  name: 19-scorecards-v5f4pbxglm62
spec:
  instances:
    app-v1:
      backend: backend
      gateway: gateway
    app-v2:
      backend: backend
      gateway: gateway
    kube-system:
      platform-dns: platform-dns
      platform-storage: platform-storage
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: 192aaf58bs5chnywmnlgix3s6qg0qi1z,95gmkzkbdf1k30pp3k54hp8vc8qd54p8,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: ksz9y8ygfpc5bzbpxq1kz9pn4ahqjgxb
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: backend
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v1
spec:
  instanceName: backend
  namespaceName: app-v1
  version: "1.30.5"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,ysmf49biwrwmi63smk3f39iapybg43jb,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: 35cl4xdxp4ivq0y758j4d7ks65i08dm5
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: gateway
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v1
spec:
  instanceName: gateway
  namespaceName: app-v1
  version: "0.0.2"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: 42s2njbqlf3cxh44ny28ajxn44z4ycdc,lxi0gciwfx6mgp92319mjxhjwvgkdqhs,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: h87xkj4fjz6ngyi6vq4qpdqwdf5rcxgk
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: backend
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v2
spec:
  instanceName: backend
  namespaceName: app-v2
  version: "1.30.5"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: lxi0gciwfx6mgp92319mjxhjwvgkdqhs,qwaclll2gzrb91xf2frqzk9yrk0fhg60,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: pq911a8cdinlzlhd4aaw2fnqz0brr1hr
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
    kix.run/root: gateway
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v2
spec:
  instanceName: gateway
  namespaceName: app-v2
  version: "0.0.2"
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: jh345cnnffy1bnj4cz5x0hwq6ibxvkkr
    kix.run/import-apiversion: apps/v1
    kix.run/import-fqdn: kube-dns.kube-system.svc.cluster.local
    kix.run/import-kind: Deployment
    kix.run/import-name: coredns
    kix.run/mode: import
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/instance: platform-dns
    app.kubernetes.io/managed-by: kix
  name: platform-dns
  namespace: kube-system
spec:
  instanceName: platform-dns
  mode: import
  namespaceName: kube-system
---
apiVersion: kix.run/v1alpha1
kind: PackageInstance
metadata:
  annotations:
    kix.run/depends-on: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m,requires:pk4hih6jm4yj336rlaqk2qycz2k46xvs
    kix.run/identity-hash: r5sgwjk8w3m41l0xaj0s16xxsacd0nk6
    kix.run/import-apiversion: storage.k8s.io/v1
    kix.run/import-kind: StorageClass
    kix.run/import-name: standard
    kix.run/mode: import
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/instance: platform-storage
    app.kubernetes.io/managed-by: kix
  name: platform-storage
  namespace: kube-system
spec:
  instanceName: platform-storage
  mode: import
  namespaceName: kube-system
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 8080;
        location / {
            default_type text/plain;
            return 200 'Response from app-v1\n';
        }
        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8
    kix.run/identity-hash: 5dj5rd7lw2dk1pv9pcnf6dpi46i93z47
    kix.run/package: backend
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v1
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 80;
        client_max_body_size 10m;

        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
        location /v1/ {
            proxy_pass http://backend.app-v1.svc.cluster.local:80/;
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: 192aaf58bs5chnywmnlgix3s6qg0qi1z,95gmkzkbdf1k30pp3k54hp8vc8qd54p8
    kix.run/identity-hash: p1v3s0pi2ksrbbpw8mmdxnb57clwmzl4
    kix.run/package: gateway
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v1
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 8080;
        location / {
            default_type text/plain;
            return 200 'Response from app-v2\n';
        }
        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/identity-hash: 3az3y9dajri5bycnprn4kc14y884dypp
    kix.run/package: backend
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v2
---
apiVersion: v1
data:
  default.conf: |
    server {
        listen 80;
        client_max_body_size 10m;

        location /healthz {
            default_type text/plain;
            return 200 'ok\n';
        }
        location /v2/ {
            proxy_pass http://backend.app-v2.svc.cluster.local:80/;
        }
    }
kind: ConfigMap
metadata:
  annotations:
    kix.run/depends-on: 42s2njbqlf3cxh44ny28ajxn44z4ycdc,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/identity-hash: 1bw942wwr3a5jy0h5mgv7zrmi97lhpm6
    kix.run/package: gateway
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v2
---
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kix.run/identity-hash: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kubernetes.io/metadata.name: app-v1
  name: app-v1
---
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kix.run/identity-hash: lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kubernetes.io/metadata.name: app-v2
  name: app-v2
---
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kix.run/identity-hash: sj0fv5bs2hnh9zcqdh62ph34l2wm0b1m
    kix.run/package: _cluster
    kix.run/package-namespace: _cluster
  labels:
    app.kubernetes.io/managed-by: kix
    kubernetes.io/metadata.name: kube-system
  name: kube-system
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,9ph1i0ympz81gpl8xxvdriykp6q5c6lq
    kix.run/identity-hash: 192aaf58bs5chnywmnlgix3s6qg0qi1z
    kix.run/package: backend
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v1
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/name: backend
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 95gmkzkbdf1k30pp3k54hp8vc8qd54p8,yc2asr6hm9fawjk7f5kcmc1r7sq83wrx
    kix.run/identity-hash: ysmf49biwrwmi63smk3f39iapybg43jb
    kix.run/package: gateway
    kix.run/package-namespace: app-v1
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v1
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/name: gateway
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 7mlywnw2vzddhhag58w05y8sbk9p6pms,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/identity-hash: 42s2njbqlf3cxh44ny28ajxn44z4ycdc
    kix.run/package: backend
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/managed-by: kix
  name: backend
  namespace: app-v2
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app.kubernetes.io/instance: backend
    app.kubernetes.io/name: backend
  type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    kix.run/depends-on: 84760c477szfqhg7ihq3ni5a1x1a7z9r,lxi0gciwfx6mgp92319mjxhjwvgkdqhs
    kix.run/identity-hash: qwaclll2gzrb91xf2frqzk9yrk0fhg60
    kix.run/package: gateway
    kix.run/package-namespace: app-v2
  labels:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/managed-by: kix
  name: gateway
  namespace: app-v2
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app.kubernetes.io/instance: gateway
    app.kubernetes.io/name: gateway
  type: ClusterIP

To make error-severity scorecard findings fail the build instead of being capped at "warning", first add maxSeverity = "error"; to tutorials/19-scorecards/scorecard.nix:

tutorials/19-scorecards/scorecard.nix
{
enable = true;
maxSeverity = "error";
# ...
}

Next, open tutorials/19-scorecards/cluster.nix and comment out maxBodySize = "10m"; on the gateway in instances.app-v1 so it falls back to the "1m" default:

tutorials/19-scorecards/cluster.nix
gateway = {
package = reverseProxyPackage;
config = {
proxyPath = "/v1/";
# maxBodySize = "10m";
};
deps.backend = ref.app-v1.backend;
};

Now run kix check 19-scorecards:

kix-examples/
❱ kix check 19-scorecards

Because custom.proxyMaxBodySizeMustBeExplicit has severity = "error" and scorecard.maxSeverity = "error" is set, evaluation fails and prints:

[scorecard:custom.proxyMaxBodySizeMustBeExplicit] app-v1/gateway: instance 'gateway' uses the default maxBodySize '1m'. nginx rejects larger request bodies - set this explicitly.

Uncomment maxBodySize = "10m"; in instances.app-v1.gateway.

Next, in tutorials/19-scorecards/cluster.nix, change proxyPath in instances.app-v2.gateway from "/v2/" to "/v1/" so both namespaces claim "/v1/":

tutorials/19-scorecards/cluster.nix
instances.app-v2.gateway = {
package = reverseProxyPackage;
config = {
proxyPath = "/v1/";
maxBodySize = "10m";
};
deps.backend = ref.app-v2.backend;
};

Run kix check 19-scorecards again:

kix-examples/
❱ kix check 19-scorecards

This time, custom.noProxyPathCollision catches the cross-namespace conflict at build time:

[scorecard:custom.noProxyPathCollision] : 2 reverse-proxy gateways share proxyPath '/v1/': app-v1/gateway, app-v2/gateway

Change app-v2’s proxyPath back to "/v2/" and remove maxSeverity = "error"; from tutorials/19-scorecards/scorecard.nix so the example returns to its checked-in state, then run kix check 19-scorecards to verify the cluster passes again.

You completed the 12-part Kix tutorial path:

  • scorecard.enable = true runs compile-time policy checks across your cluster
  • scorecard.maxSeverity ("info", "warning", or "error", defaulting to "warning") caps finding severity across the cluster, and setting maxSeverity = "error" makes error-severity rules fail evaluation
  • level = "package" rules inspect an individual instance’s meta, config, and resources without parsing rendered YAML strings
  • level = "cluster" rules inspect allInstances across all namespaces to catch cross-instance collisions before deployment
  • Browse the How-to guides for step-by-step recipes on secrets, CRDs, Helm chart adoption, NetworkPolicies, and scorecard customization.
  • Read the Explanation pages for deeper dives into the dependency graph, availability synthesis, NetworkPolicy derivation, and Server-Side Apply field ownership.
  • Keep the Reference handy while authoring your own clusters and packages.