Configuration
Reference for helm/norviq/values.yaml at chart version 0.2.5. Every default below was read from
that file; where the chart’s inline comment records why a default is what it is, that reasoning is
summarised here. The chart’s comments are worth reading directly — several of them carry the
measurements the default was chosen from.
For the install walkthrough see Deployment; for a quick single-cluster evaluation see Get started. This page is the value-by-value reference behind both.
The chart declares kubeVersion: ">=1.30.0-0". helm install refuses on an older API server.
Overlays
Section titled “Overlays”Three overlays ship alongside the defaults:
| File | What it changes |
|---|---|
helm/norviq/values-prod.yaml |
Multi-node production posture. pullPolicy: IfNotPresent on the four app components; api/engine/webhook get PDBs, HPAs and node spread; webhook.injection.enabled: true; api resources are re-pinned to req 250m/256Mi, lim 1/512Mi and opa to lim 500m/256Mi; postgresql.ha.enabled + redis.ha.enabled with postgresql.storage: 20Gi; config.dbSslMode: require; gracefulShutdown.preStopSleepSeconds: 5; and every shipped datastore/fleet password is blanked so the operator must supply one. Not live-validated on a single-node cluster; template-validated only. Requires ≥3 nodes, metrics-server, the CloudNativePG operator and a RedisFailover operator. |
helm/norviq/values-light.yaml |
The smallest viable single-node footprint. Enforcement is byte-identical to the default — it only changes replica counts, PDBs and resource envelopes (api.replicas 1, api.workers 1, api memory limit 384Mi, no api PDB, engine.replicas 1, webhook.replicas 1). The optional planes it pins off (fleet, HA, HPA, SPIFFE CSI) are already off by default; it pins them to be explicit. |
helm/norviq/values-dev.yaml |
Dev cluster: a fixed non-secret api.secretKey, fixed dev Redis/Postgres passwords, postgresql.storage: 1Gi, config.logLevel: DEBUG, config.enforcementMode: audit, pullPolicy: Always on engine/api/ui. |
Apply one with:
helm install norviq oci://ghcr.io/norviq-dev/charts/norviq \ --version 0.2.5 \ -n norviq --create-namespace \ -f helm/norviq/values-prod.yaml \ --set policyQuotaNamespaces='{prod-agents}'--set always wins over a -f file for the same key.
Settings that fail the install on purpose
Section titled “Settings that fail the install on purpose”The chart refuses to render several combinations rather than produce a deployment whose failure would surface far from its cause. These are deliberate; the error text tells you what to set.
| Condition | Where | Why the chart refuses |
|---|---|---|
baselineClusterPolicy.enabled: true (the default) with policyQuotaNamespaces: [] (the default) |
templates/baseline-cluster-policy.yaml |
The baseline renders one NrvqPolicy per listed namespace. With the list empty it would render zero baselines while NOTES.txt claims a baseline is installed — a silent gap. A pure-defaults helm install therefore fails. Either set policyQuotaNamespaces, or set baselineClusterPolicy.enabled=false to run without a cluster baseline explicitly. |
A namespace in policyQuotaNamespaces does not exist |
templates/resource-quota.yaml |
ResourceQuota and NrvqPolicy are namespaced. The pre-flight uses lookup, and first probes for kube-system — if that probe comes back empty the chart knows it is rendering offline (helm template, --dry-run=client) and stays silent rather than failing on missing information. kubectl create namespace <ns> first. |
api.resources.limits.memory ÷ api.workers below 128Mi |
templates/api-deployment.yaml |
Each uvicorn worker is a full process (~150Mi under load). Mismatched, the pod OOMKills in a loop and the install times out on “Pending termination” — which points at Helm rather than at the two numbers that disagree. The floor is 128Mi rather than 150Mi so it catches the obviously-wrong profile without second-guessing an operator running deliberately close to the line. |
otel.enabled: true with an empty otel.endpoint |
templates/configmap.yaml |
Trace export with no collector silently drops every span. |
oidc.enabled: true with an empty issuer, jwksUrl or audience |
templates/configmap.yaml |
Each is required to validate a token. |
<component>.autoscaling.enabled: true with neither CPU nor memory target set |
templates/hpa.yaml |
An HPA with no metric is invalid. |
redis.ha.enabled: true with an empty redis.password |
templates/redis-ha.yaml |
The Sentinel cluster takes its password through customConfig and cannot use the chart-generated one. |
postgresql.ha.enabled: true with an empty postgresql.password |
templates/postgresql-ha.yaml |
CloudNativePG manages its own credential Secret and cannot use the chart-generated one. |
postgresql.enabled: false (or redis.enabled: false) with neither host nor existingSecret |
templates/secret.yaml |
The chart will not render an address it would only guess wrong. |
openshift.enabled: true with the bundled Postgres/Redis still on |
templates/validate-openshift.yaml |
The official postgres/redis entrypoints start as root to chown their data dir, so they cannot run under OpenShift’s restricted-v2 SCC. Point the chart at datastores you manage. |
agentEgressPolicy.enabled: true with no namespaces, an unknown engine, or the control-plane namespace in the list |
templates/agent-egress-*.yaml |
An egress policy that protects nothing, or one aimed at Norviq’s own namespace. |
agentEgressPolicy.embeddedDatastores: true while Redis/Postgres are external |
templates/agent-egress-*.yaml |
No podSelector can reach an out-of-cluster datastore; add its CIDR to allowedCIDRs. |
config.requireStrongSecret: true with fleet.hub.postgresql.password empty or still norviq_dev (or that password inside fleet.hub.pgUrl) |
templates/fleet-hub.yaml |
The shipped fleet DB password is a dev default. |
webhook.injection.mcp.enabled: true with an empty webhook.injection.mcp.proxyImage |
templates/webhook-deployment.yaml |
A Helm required with no fallback — see MCP injection. |
One combination deliberately does not fail: webhook.injection.credentialSecret.enabled: true
with no namespaces to bind in. See Injected credentials.
Values are schema-validated
Section titled “Values are schema-validated”helm/norviq/values.schema.json is checked by Helm on install, upgrade, template and lint, so a typo
in a load-bearing enum fails with a path and a message before anything is applied. It is deliberately
permissive (additionalProperties allowed) so --set of any leaf keeps working; only types, enums and
ranges that matter are constrained:
| Key | Accepted values |
|---|---|
config.enforcementMode |
block | audit — there is no allow/monitor value |
config.noPolicyDecision |
allow | deny |
config.opaMode |
server | subprocess |
config.dbSslMode |
"" | disable | allow | prefer | require | verify-ca | verify-full |
config.trustThreshold, config.violationPenalty |
number, 0–1 |
webhook.injection.sidecarMode |
proxy | embedded |
webhook.injection.failurePolicy |
Fail | Ignore |
agentEgressPolicy.engine |
networkpolicy | cilium |
baselineClusterPolicy.preset |
strict | moderate | permissive |
images.*.pullPolicy |
Always | IfNotPresent | Never |
baselineClusterPolicy.enforcementMode is not in the values schema. It is validated one layer
later by the NrvqPolicy CRD, whose enum is block | audit | escalate — so a typo there fails at
apply time rather than at render time.
Production checklist
Section titled “Production checklist”| Key | Default | Why it matters |
|---|---|---|
api.secretKey |
change-me-in-production (sentinel) |
Left at the sentinel the chart auto-generates a strong random JWT signing secret on first install and persists it across upgrades via a live lookup, so upgrades never invalidate existing sessions. Set an explicit value only to pin your own (rotation, multi-cluster fleet trust) — an explicit value always wins. |
config.requireStrongSecret |
true |
Fail-closed: the API refuses to start on a weak/default/short JWT secret or the default admin password. Turn it off only for a throwaway cluster. |
imagePullSecrets |
[] |
Empty is correct for the public ghcr.io/norviq-dev images. Set a pull-secret name only if you point images.registry at a private registry. The list is copied verbatim into each pod spec, so entries take the Kubernetes shape: - name: my-pull-secret. |
config.dbSslMode |
"" (derived) |
Derived from the datastore you actually chose — see below. An explicit value is honoured verbatim. |
auth.requireBoundAgentIdentity |
true |
Every non-admin caller must present a credential bound to an agent class (403 otherwise). See Local auth. |
policyQuotaNamespaces |
[] |
Must be set (or the baseline disabled) or the install fails — see above. |
How config.dbSslMode is derived
Section titled “How config.dbSslMode is derived”flowchart TD
A["config.dbSslMode set explicitly?"] -->|yes| B["use it verbatim"]
A -->|no| C{"which datastore?"}
C -->|"bundled StatefulSet<br/>(postgresql.enabled, no host, no ha)"| D["disable"]
C -->|"CloudNativePG HA"| E["require"]
C -->|"external / managed host"| F["require"]
The bundled single-node Postgres has no TLS listener, so require there could only crash on startup —
which is exactly what a hard require default used to do to a pure-defaults install. A managed
database still gets require with no operator action. values-prod.yaml pins require explicitly.
Cluster-wide settings
Section titled “Cluster-wide settings”| Key | Default | What it does |
|---|---|---|
global.imageRegistry |
"" |
Air-gapped/private-mirror host prepended to the third-party images only (opa, redis, postgres, the internal-TLS nginx, the helm test curl, and an explicitly-set crdFinalizerCleanup.image). Norviq’s own images — engine, api, ui, webhook and bootstrap — use images.registry instead. Mirror the upstream images preserving their path. |
policyQuotaNamespaces |
[] |
The tenant-namespace list. Two things read it: a ResourceQuota capping count/nrvqpolicies.norviq.io at 100 per listed namespace, and baselineClusterPolicy, which renders one guard policy per listed namespace. It is also the fallback for agentEgressPolicy.namespaces and webhook.injection.credentialSecret.namespaces. NrvqClass and NrvqConfig are cluster-scoped and bounded by RBAC, not by this quota. |
imagePullSecrets |
[] |
Pull secrets for the component images. |
podAnnotations |
{} |
Extra annotations stamped onto every Norviq pod template. Chiefly a rollout lever — a value that changes per build (image ID, git sha) forces the Deployment to roll, which a rebuilt-but-same-tag image otherwise will not. |
tests.image |
curlimages/curl:8.10.1 |
Connection-probe pod for helm test. Routes through global.imageRegistry. |
openshift.enabled |
false |
On OpenShift, omit runAsUser/runAsGroup/fsGroup entirely and let the platform assign a UID from the namespace’s range, while keeping runAsNonRoot: true. A chart that pins runAsUser is rejected by restricted-v2 no matter which UID it pins. Leave off for vanilla Kubernetes/AKS/EKS/GKE, where pinning an explicit non-root UID is the stronger posture. |
Baseline cluster policy (baselineClusterPolicy.*)
Section titled “Baseline cluster policy (baselineClusterPolicy.*)”| Key | Default | What it does |
|---|---|---|
baselineClusterPolicy.enabled |
true |
For each namespace in policyQuotaNamespaces, renders one NrvqPolicy in the release namespace whose spec.target.namespace is that tenant namespace. |
baselineClusterPolicy.name |
baseline-cluster-guard |
Rendered as <name>-<ns>. |
baselineClusterPolicy.clusterPriority |
900 |
Priority band of the rendered guard. |
baselineClusterPolicy.preset |
strict |
Which preset the guard runs. |
baselineClusterPolicy.enforcementMode |
audit |
audit — every baseline control evaluates and records a non-compliance event, and the call proceeds. block — the baseline refuses the call outright. escalate is also accepted by the CRD. |
Agent egress policy (agentEgressPolicy.*)
Section titled “Agent egress policy (agentEgressPolicy.*)”Norviq’s tool-call PEP is cooperative — the agent’s SDK asks the sidecar for a forward/drop decision and the agent executes the tool itself, so a pod that ignores the SDK can reach tools directly. This opt-in default-deny egress policy bounds that at the network layer: an agent pod may egress only to the Norviq API, DNS, and an operator-approved allowlist. It does not replace the PEP (per-call parameter policy still needs the SDK) and it requires a NetworkPolicy-enforcing CNI — Calico or Cilium; kindnet ignores NetworkPolicy entirely.
| Key | Default | What it does |
|---|---|---|
agentEgressPolicy.enabled |
false |
Render the policy. |
agentEgressPolicy.engine |
networkpolicy |
networkpolicy — a portable Kubernetes NetworkPolicy; the allowlist is IP/CIDR only. cilium — a CiliumNetworkPolicy that also supports FQDN allowlisting. Anything else fails the render. |
agentEgressPolicy.namespaces |
[] |
Namespaces to lock down. Empty reuses policyQuotaNamespaces; if both are empty the render fails. Listing the Norviq control-plane namespace also fails the render. |
agentEgressPolicy.allowDNS |
true |
Permit DNS egress. |
agentEgressPolicy.allowedCIDRs |
[] |
Approved tool endpoints. Everything else, including the internet, is denied. Leave empty only if every tool the agent calls lives in the Norviq namespace — otherwise its tool calls fail. |
agentEgressPolicy.allowedFQDNs / allowedFQDNPatterns |
[] |
engine: cilium only — egress by hostname via Cilium’s DNS-aware toFQDNs. Exact names in the first, wildcards in the second. |
agentEgressPolicy.allowedPorts |
[] |
Restrict the allowlist to these TCP ports; empty means all ports. |
agentEgressPolicy.embeddedDatastores |
false |
embedded sidecar mode reaches Redis/Postgres directly rather than the API. Set true to also permit egress to the Norviq datastores. Leave false for the default proxy mode. |
Uninstall cleanup (crdFinalizerCleanup.*)
Section titled “Uninstall cleanup (crdFinalizerCleanup.*)”| Key | Default | What it does |
|---|---|---|
crdFinalizerCleanup.enabled |
true |
A pre-delete hook Job that releases the norviq.io/policy-protection finalizer on every NrvqPolicy while the API is still up. Without it, Helm removes the controller in the same uninstall that needs it, the CRs strand in Terminating, the uninstall blocks until it times out, and the release is left half-removed. Best-effort by design — it never fails the uninstall. Disable only if your cluster forbids hook Jobs, and then strip the finalizers yourself before helm uninstall. |
crdFinalizerCleanup.image |
"" |
Empty uses the first-party bootstrap image, which is what you want. This defaulted to bitnami/kubectl:1.31 until Bitnami removed those tags from Docker Hub — the pull 404s, the hook never runs, and every CR strands. Set this only to pin an image you mirror yourself; it must provide /bin/sh, curl and jq. An explicit value routes through global.imageRegistry. |
Images / registry
Section titled “Images / registry”| Key | Default | What it does |
|---|---|---|
images.registry |
ghcr.io/norviq-dev/ |
Prefix (with trailing slash) prepended to every Norviq component repository. Override to ghcr.io/<your-org>/, an Artifact Registry/ACR path, or "" plus a Docker Hub repository. |
images.{engine,api,ui,webhook,bootstrap}.repository |
norviq-engine |
One multi-stage build produces all five; they differ only by tag. |
images.{engine,api,ui,webhook,bootstrap}.tag |
engine-latest / api-latest / ui-latest / webhook-latest / bootstrap-latest |
Per-component tag within the shared repository. |
images.*.digest |
"" |
Empty in the source tree and stamped by the release workflow. When set it wins over tag, so --version 0.2.5 pulls the exact image built, scanned and signed for 0.2.5. A chart pulled from the OCI registry is digest-pinned even though the in-tree values.yaml is not. |
images.{engine,api,ui,webhook,bootstrap}.pullPolicy |
Always |
values-prod.yaml sets IfNotPresent for the four app components. |
images.redis.repository/tag |
redis / 7-alpine |
Bundled Redis image. |
images.postgresql.repository/tag |
postgres / 16-alpine |
Bundled Postgres image. |
The bootstrap image is first-party and carries openssl (self-sign the internal CA) and curl
(talk to the API server with the pod’s service-account token). It deliberately does not carry
kubectl: vendoring that Go binary pulled its CVEs into Norviq’s own fail-closed Trivy gate, which
then refused to publish the image. The hooks only ever GET/PATCH a MutatingWebhookConfiguration and
apply a Secret — plain authenticated REST calls. Baking openssl in also removes a network dependency
from the install path, so the hooks work air-gapped, and it avoids the apk add that the previous
alpine/k8s image needed, which requires root and is rejected outright by OpenShift’s restricted-v2
SCC. Because it is first-party it resolves through images.registry, not global.imageRegistry.
Third-party images (opa.image, images.redis, images.postgresql, config.internalTls.proxyImage,
postgresql.ha.image, tests.image, an explicit crdFinalizerCleanup.image) route through
global.imageRegistry for air-gapped mirroring.
Pod hardening and shutdown
Section titled “Pod hardening and shutdown”| Key | Default | What it does |
|---|---|---|
securityContext.enabled |
true |
Applies a restricted container profile to the primary app containers and the opa/tls-proxy sidecars. Set inline rather than relying on an external PSA/Kyverno mutation that need not exist on the target cluster. The UI (nginx) is not covered by this default — it gets a component-specific uid/port/emptyDir treatment. Set false to defer entirely to a cluster-level PodSecurity/Kyverno policy. |
securityContext.runAsNonRoot |
true |
|
securityContext.allowPrivilegeEscalation |
false |
|
securityContext.readOnlyRootFilesystem |
true |
A consumer whose container writes its root FS must supply a writable emptyDir. |
securityContext.capabilities.drop |
["ALL"] |
|
securityContext.seccompProfile.type |
RuntimeDefault |
|
gracefulShutdown.preStopSleepSeconds |
15 |
preStop sleep on api/engine/webhook. This is the window in which the pod’s removal from the Service endpoints propagates to every kube-proxy. Until it lands, traffic still routes to a terminating pod — and for an enforcement point that traffic does not merely fail, the engine fails closed and the agent’s tool call is refused. Measured (delete one replica under ~1,500 concurrent evaluate calls): at 3, 1 and 31 benign calls refused across two runs; at 15, 0, 0 and 2 across three runs. Never once in the fail-open direction. 0 disables the hook. values-prod.yaml pins 5. |
API (api.*)
Section titled “API (api.*)”| Key | Default | What it does |
|---|---|---|
api.replicas |
2 |
HA default: survive a single pod restart/drain without an enforcement gap. |
api.pdb.enabled / minAvailable |
true / 1 |
Keep at least 1 API pod available during voluntary disruptions. |
api.autoscaling.enabled |
false |
HPA; needs metrics-server. minReplicas/maxReplicas/targetCPUUtilizationPercentage = 2/6/70; targetMemoryUtilizationPercentage "" (do not scale on memory). When enabled the Deployment omits replicas so it does not fight the autoscaler. |
api.spread.enabled |
false |
podAntiAffinity + topologySpreadConstraints across nodes. |
api.rollout.maxSurge/maxUnavailable |
1 / 0 |
Zero-downtime rolling update; needs node headroom for the surge pod. |
api.resources.requests |
100m / 128Mi |
Requests drive scheduling and are deliberately left small — a 1000m request did not fit at all on a 2-node/2-vCPU AKS cluster. |
api.resources.limits |
2000m / 1Gi |
Only the limits are generous. At a 500m limit this pod was throttled on 92.5% of CFS periods under sustained single-client load — 23.8s of throttle across 400 evaluations, ~60ms added to every call; raising the limit alone took caller-observed p50/p95 from 115/172ms to 62/101ms with zero throttled periods. Memory got the same treatment: a single worker idled at 214Mi against the old 256Mi limit. Lower these only to deliberately cap a noisy tenant — the cost is paid as p95 latency on every enforcement decision. |
api.workers |
4 |
Uvicorn worker processes. With one worker a pod served one request at a time regardless of its CPU limit: measured p50 66 → 131 → 291ms at concurrency 1/2/4, and past ~4 concurrent it crossed the engine’s 2s budget and failed closed, refusing 92% of ordinary calls. Track this to the CPU limit; raising it needs memory to match, and the chart fails the render if it does not. |
api.port |
8080 |
Container port. Prometheus metrics are served at /metrics on this same port. |
api.secretKey |
change-me-in-production |
See the production checklist. |
Engine (engine.*)
Section titled “Engine (engine.*)”The standalone evaluation engine. Mirrors the API’s HA knobs at a smaller scale:
| Key | Default |
|---|---|
engine.replicas |
1 |
engine.pdb.enabled / minAvailable |
false / 1 |
engine.autoscaling.enabled |
false (minReplicas 2, maxReplicas 6, CPU target 70, memory target "") |
engine.spread.enabled |
false |
engine.rollout.maxSurge / maxUnavailable |
1 / 0 |
engine.resources |
req 100m/128Mi, lim 500m/256Mi |
engine.port |
8282 |
UI (ui.*)
Section titled “UI (ui.*)”| Key | Default | What it does |
|---|---|---|
ui.replicas |
1 |
Console pod count. |
ui.fleetApiUrl |
"" |
Set to /fleet-api on the hub cluster to show the multi-cluster Fleet view (same-origin, proxied by nginx to norviq-fleet-api). Empty on spokes and single-cluster installs — the Fleet view stays gated off. |
ui.rollout.maxSurge/maxUnavailable |
1 / 0 |
Zero-downtime rollout. |
ui.resources |
req 50m/64Mi, lim 200m/128Mi |
Per-pod CPU/memory. |
ui.port |
8080 |
The console runs on the unprivileged nginx image (uid 101) binding 8080, so the container is non-root and needs no NET_BIND_SERVICE. Its nginx also proxies /api/* and /ws/* to norviq-api, so one host serves UI and API. The Service and probes reference the named http port, so a change here cascades. |
Webhook (webhook.*)
Section titled “Webhook (webhook.*)”| Key | Default | What it does |
|---|---|---|
webhook.enabled |
true |
Deploy the admission webhook server (which also hosts the CRD controller). |
webhook.replicas |
2 |
Pod count. |
webhook.pdb.enabled / minAvailable |
false / 1 |
Off by default because a PDB blocks node drains on a single-node cluster. On a multi-node cluster turn this on — otherwise one drain can evict both replicas and reject pod creation in every governed namespace until they reschedule. |
webhook.autoscaling.enabled |
false |
2–4 replicas @ 70% CPU when enabled. |
webhook.spread.enabled |
true |
On by default, unlike the other components: the injector is the only workload whose unavailability rejects pod creation. Both replicas on one node turns a routine drain into a pod-creation outage across every governed namespace — observed on a 2-node cluster during a drill. The constraint is soft (whenUnsatisfiable: ScheduleAnyway), so single-node clusters still schedule both. |
webhook.rollout.maxSurge/maxUnavailable |
1 / 0 |
Zero-downtime rollout. |
webhook.port |
8443 |
Admission server port. |
webhook.resources |
req 50m/64Mi, lim 200m/128Mi |
Per-pod CPU/memory. |
webhook.spiffe.inject |
false |
Inject the SPIFFE Workload API socket + workload-api mode into injected workloads. Needs SPIRE and the SPIFFE CSI driver. |
webhook.oidc.enabled |
false |
The controller mints its API bearer via OIDC client-credentials instead of the HS256 service token. |
webhook.oidc.tokenUrl / clientId / clientSecret |
"" / norviq-webhook / "" |
Token endpoint, client id and confidential-client secret (rendered into norviq-secrets when enabled). |
Sidecar injection (webhook.injection.*)
Section titled “Sidecar injection (webhook.injection.*)”| Key | Default | What it does |
|---|---|---|
webhook.injection.enabled |
false |
Turnkey sidecar injection: renders the MutatingWebhookConfiguration plus a pre/post-install hook Job that self-signs a TLS cert and patches the webhook’s caBundle — no cert-manager. Enable, then label target namespaces norviq-injection=enabled. values-prod.yaml turns this on. |
webhook.injection.sidecarMode |
proxy |
proxy — the sidecar POSTs each tool call to the central norviq-api /evaluate with a namespace-scoped service JWT; DB and OPA stay centralized. embedded — the sidecar runs its own Redis cache, OPA subprocess and policy loader, for air-gapped/edge use; the chart then wires NRVQ_REDIS_URL/NRVQ_PG_URL through to the injector. |
webhook.injection.gateOnlyAgentPods |
true |
Gate admission on the pod’s own norviq.io/agent-class label rather than the whole namespace. Without it, norviq-injection=enabled routes every pod created in that namespace through the injector — databases, ingress controllers, batch jobs — so with failurePolicy: Fail a Norviq outage blocks pod creation for workloads Norviq does not govern. CREATE-only, so running pods are never touched. |
webhook.injection.failurePolicy |
Fail |
Admission posture. Fail is fail-closed: if the injector is unavailable, creation of a pod the webhook routes is rejected, so a routed pod can never start un-guarded. kube-system, kube-public, kube-node-lease and the release namespace are excluded from the selector, as are control-plane=true and AKS-managed namespaces, so this never self-deadlocks. Note what it does not cover: with gateOnlyAgentPods: true, an unlabelled pod is not routed and starts ungoverned regardless of this setting. Set Ignore only for a dev/eval cluster. |
webhook.injection.allowPodOptOut |
true |
Honour the per-pod opt-out (norviq-injection=disabled label / norviq.io/skip-injection annotation). Set false so a routed pod cannot self-exempt. This is not namespace-uniform while gateOnlyAgentPods is true — omitting norviq.io/agent-class is an earlier exit this does not close. Pair with RBAC on the agent-class label as well as on the opt-out label/annotation. |
webhook.injection.fallbackMode |
allow |
Data-plane posture — see below. |
webhook.injection.sidecarResources.proxy |
req 50m/64Mi, lim 200m/128Mi |
A thin forwarder. |
webhook.injection.sidecarResources.embedded |
req 200m/256Mi, lim 2000m/384Mi |
Measured, not guessed. The request sits above the measured peak (214Mi after 80 evaluations) on purpose — an enforcement sidecar evicted under node pressure fails the workload it guards. The 2000m CPU limit is load-bearing: measured back-to-back on AKS, proxy p50 59.0ms / p95 92.7ms, embedded @ 500m p50 72.0ms / p95 93.1ms with 58.9% of CFS periods throttled, embedded @ 2000m p50 30.7ms / p95 58.0ms with 0% throttled. At 500m, embedded was slower than the default it exists to beat. |
Injected credentials (webhook.injection.credentialSecret.*)
Section titled “Injected credentials (webhook.injection.credentialSecret.*)”| Key | Default | What it does |
|---|---|---|
credentialSecret.enabled |
true |
Deliver the injected sidecar’s NRVQ_API_TOKEN and mTLS client cert/key through a Secret + valueFrom.secretKeyRef instead of literal value: entries in the pod spec. Kubernetes deliberately excludes Secrets from the built-in view ClusterRole (view grants get pods, not get secrets), so a credential in the pod spec is readable by anyone a read-only grant was considered safe for — and it lands in etcd, kubectl describe and any GitOps diff. |
credentialSecret.required |
false |
The webhook needs namespaced Secret write; the chart renders a Role + RoleBinding per namespace (not a ClusterRole — the injector already holds admission-time mutation power over every pod in labelled namespaces, and cluster-wide secret write would make compromising it far more valuable). If it cannot write the Secret it falls back to literal pod env and logs NRVQ-WHK-4049 at ERROR for every injected pod rather than refusing to schedule, because this webhook runs failurePolicy: Fail. Set true to refuse instead. |
credentialSecret.namespaces |
[] |
Namespaces labelled norviq-injection=enabled that get the Role/RoleBinding. Empty defaults to policyQuotaNamespaces. |
If both lists are empty the chart renders no Role/RoleBinding and does not fail. That is
deliberate: “injection on, no tenant namespaces declared” turned out to be a mainstream configuration,
and breaking every such upgrade to close a read-exposure was the wrong trade. The exposure is not
silent — the per-pod NRVQ-WHK-4049 ERROR names both the consequence and the remedy.
MCP action-firewall injection
Section titled “MCP action-firewall injection”webhook.injection.mcp.* gives Model Context Protocol traffic the same zero-code-change deal the
sidecar gives the SDK. A pod opts in per container:
metadata: annotations: norviq.io/mcp-servers: "filesystem,github" # containers whose command IS an MCP server norviq.io/mcp-server-id.github: "github-prod" # optional stable pin id (default: the container name)The injector rewrites each named container’s command to exec the proxy, and an init container copies
that proxy in from proxyImage — so the MCP server’s own image needs nothing installed.
Two admission rules follow from “never leave a named server ungoverned”: a name that matches no
container, and a container with no explicit command (its argv is the image ENTRYPOINT, which
admission cannot see), are both denied rather than skipped.
| Key | Default | What it does |
|---|---|---|
webhook.injection.mcp.enabled |
false |
Turn on MCP injection. |
webhook.injection.mcp.proxyImage |
"" |
Required when enabled, with no fallback — the render fails without it. Must hold the relocatable payload at proxySourcePath; build it with scripts/mcp-proxy-payload.Dockerfile and confirm it runs in the images your MCP servers actually use with scripts/mcp-proxy-payload-verify.sh. Defaulting this to the engine image was wrong — that image carries the norviq package, not the frozen payload, so the init container’s cp found nothing and every governed pod failed at start. |
webhook.injection.mcp.proxySourcePath |
/opt/norviq/mcp-proxy |
Where the payload lives inside proxyImage. |
webhook.injection.mcp.pinStore |
control-plane |
Where Gate-A definition pins live. control-plane is the right posture in a cluster: pins are approvals, so they belong with policy — tenant-scoped, RBAC’d, audited, console-visible, and not lost when a pod restarts. memory/file exist for air-gapped single-process use. |
webhook.injection.mcp.pinMode |
tofu |
tofu trusts a server’s definitions on first sight; strict quarantines them until an operator approves. strict is safer and needs an approval workflow to be practical. |
Everything under webhook.injection.mcp.* is inert unless the MCP proxy is actually running, so
turning it on changes nothing about existing SDK-governed traffic.
Redis (redis.*)
Section titled “Redis (redis.*)”| Key | Default | What it does |
|---|---|---|
redis.enabled |
true |
true runs a bundled Redis (single StatefulSet, or Sentinel under HA). false means bring your own — nothing is deployed and you set host (and usually existingSecret). |
redis.host |
"" |
External hostname. Empty uses the bundled service. Required when enabled: false. |
redis.existingSecret / existingSecretKey |
"" / url |
Production credential path: a Secret you manage holding the full Redis URL. When set, the chart never renders or generates a Redis credential, so it never passes through values, --set (which lands in helm history and your shell history), or the chart’s own Secret. |
redis.replicas |
1 |
Single instance. |
redis.ha.enabled / replicas / serviceName |
false / 3 / norviq-redis-ha |
Gated, not live-validated on a single node. Renders a Sentinel topology; requires the operator/Sentinel chart pre-installed and an explicit redis.password. The chart renders that Service itself, selecting the operator’s redisfailovers-role: master pod label so it tracks the current master through failover with no client change. |
redis.resources |
req 100m/128Mi, lim 300m/256Mi |
Per-pod CPU/memory. |
redis.port |
6379 |
|
redis.password |
"" (generated) |
Empty means the chart generates a strong random password on first install into norviq-secrets and reuses it on every upgrade. Read it with kubectl get secret norviq-secrets -o jsonpath='{.data.NRVQ_REDIS_PASSWORD}' | base64 -d. Required explicitly when redis.ha.enabled=true. |
redis.storage |
1Gi |
PVC size. |
PostgreSQL (postgresql.*)
Section titled “PostgreSQL (postgresql.*)”| Key | Default | What it does |
|---|---|---|
postgresql.enabled |
true |
true runs a bundled Postgres. false means bring your own (RDS, Cloud SQL, Azure Database) — the usual production shape. |
postgresql.host |
"" |
External hostname. Empty uses the bundled service. Required when enabled: false (or supply existingSecret). |
postgresql.existingSecret / existingSecretKey |
"" / url |
A Secret you manage holding the full Postgres URL. The URL carries user/password/host/port/database, so those keys are then unused. |
postgresql.replicas |
1 |
Single instance. |
postgresql.ha.enabled / instances / serviceName |
false / 3 / norviq-postgresql-ha-rw |
Gated, not live-validated on a single node. Renders a CloudNativePG Cluster and points the API’s PG URL at its service; requires the CNPG operator and an explicit postgresql.password. |
postgresql.ha.image |
ghcr.io/cloudnative-pg/postgresql:16 |
The CNPG operand image — deliberately not images.postgresql. CNPG hardcodes postgres UID/GID 26 and runs initdb as that UID; in postgres:16-alpine the postgres user is UID 70, so initdb aborts with could not look up effective user ID 26 and the cluster never bootstraps. |
postgresql.resources |
req 200m/256Mi, lim 500m/512Mi |
Per-pod CPU/memory. |
postgresql.port |
5432 |
|
postgresql.database / username |
norviq / norviq |
Bundled DB name and user. |
postgresql.password |
"" (generated) |
Empty means the chart generates a strong random password into norviq-secrets and reuses it on every upgrade — Postgres only honours POSTGRES_PASSWORD at initdb, so rotating it under an existing PVC would strand the database, and the chart never does. Read it with kubectl get secret norviq-secrets -o jsonpath='{.data.NRVQ_PG_PASSWORD}' | base64 -d. Required explicitly when postgresql.ha.enabled=true. |
postgresql.storage |
5Gi |
PVC size. |
OPA (opa.*)
Section titled “OPA (opa.*)”| Key | Default | What it does |
|---|---|---|
opa.enabled |
true |
Run OPA as a long-lived sidecar in every api/engine pod (localhost:8181) instead of forking opa eval per call. Each replica gets its own OPA — no shared single point of failure. |
opa.image |
openpolicyagent/opa:1.19.1-static |
Pinned (not latest-static) so the running OPA version stays matched to the one scripts/gen-opa-capabilities.py generated helm/norviq/files/opa-capabilities.json from. A drifted OPA could carry a different builtin set than norviq/engine/opa_client.py::_check_capabilities validates against. |
opa.port |
8181 |
|
opa.resources.requests |
50m / 64Mi |
|
opa.resources.limits |
1500m / 512Mi |
Sized for the compile burst, not the query path. Rego compilation is a CPU- and memory-bound burst that happens whenever a policy is created or edited, and OPA stops answering queries while it recompiles its module store. Throttled at 250m that burst ran past the engine’s 2s budget, so the first tool call after every policy change was wrongly blocked (evaluator_timeout): measured on AKS, 250m → 0/5 correct on the first call after a save, 1500m → 5/5. Memory saw the same — 117Mi against a 128Mi limit at idle, and OOMKilled during a concurrent policy push. Steady-state evaluation uses ~7m CPU and ~117Mi either way. |
config.opaMode (below) selects whether the engine talks to this sidecar (server, the default) or
falls back to a per-call opa eval fork (subprocess).
config.* — core runtime settings
Section titled “config.* — core runtime settings”| Key | Default | What it does |
|---|---|---|
config.internalTls.enabled |
true |
Zero-touch internal mTLS for control-plane traffic — on by default, no operator action. See the note below. |
config.internalTls.proxyImage |
nginx:1.27-alpine |
The API pod’s TLS terminator sidecar. |
config.internalTls.proxyResources |
req 10m/24Mi, lim 64Mi memory |
No CPU limit on the proxy. |
config.logLevel |
INFO |
values-dev.yaml sets DEBUG. |
config.enforcementMode |
block |
Global default enforcement mode (block | audit). Individual NrvqPolicy objects override per target; values-dev.yaml sets audit. |
config.noPolicyDecision |
allow |
See the caution below. |
config.opaMode |
server |
server — HTTP to the per-pod OPA sidecar. subprocess — per-call opa eval fork (rollback path). |
config.requireStrongSecret |
true |
See the production checklist. |
config.trustThreshold |
0.7 |
Agent trust score below which evaluation escalates. |
config.violationPenalty |
0.05 |
Trust-score deduction per policy violation. |
config.rateLimit |
60 |
Per-identity evaluation throttle (non-read calls per 60s). Wired since 0.2.4 — see the note below. |
config.httpRateLimitTrustedProxyHops |
1 |
See HTTP rate limiting. |
config.httpRateLimitTrustedProxyCidrs |
["127.0.0.0/8", "::1/128"] |
See HTTP rate limiting. |
config.inprocCacheTtlS |
0 (off) |
Opt-in per-pod L1 cache — see In-process cache. |
config.inprocCacheMax |
8192 |
Hard per-cache entry cap, bounding per-pod memory under identity/namespace churn. |
config.dbSslMode |
"" (derived) |
See the derivation above. |
config.dbPoolMaxOverflow / dbPoolTimeout / dbCommandTimeout |
10 / 10 / 10 |
DB connection pool tuning. |
config.spiffeMode |
mock |
mock — env-var identity, no SPIRE needed. workload-api — real SPIFFE SVID resolution, fail-closed; requires SPIRE on the cluster plus pyspiffe. |
config.spiffeSocket |
/spiffe-workload-api/spire-agent.sock |
Where the SPIFFE CSI driver publishes the agent socket. |
config.spiffeCsi.enabled |
false |
Gates the csi.spiffe.io volume on api/engine pods. Off by default so deploys without SPIRE are unaffected — the volume would otherwise wedge pod creation with no driver or registration. |
config.extraEnv — the escape hatch
Section titled “config.extraEnv — the escape hatch”norviq/config.py carries roughly a hundred tunables and the chart deliberately surfaces only the ones
an operator normally needs. config.extraEnv is the supported way to set the rest: every key is
rendered verbatim into norviq-config before the named keys, so a named value still wins where
both are set.
config: extraEnv: NRVQ_HTTP_RATE_LIMIT_REDTEAM_PER_WINDOW: "60" NRVQ_OPA_TIMEOUT_MS: "400"It is not present in values.yaml (it defaults to an empty dict) and it is not in the values schema,
so nothing validates the keys or values you put here — a typo is silently ignored by the settings
model. Use it to raise a limit for a test environment, or to try a setting before it earns a named
value; patching the ConfigMap by hand instead is reverted by the next helm upgrade.
HTTP rate limiting
Section titled “HTTP rate limiting”Separate from the evaluation throttle above, the API runs a Redis-backed fixed-window HTTP throttle in front of every route (shared correctly across HA replicas). It is on by default with a 60s window, keyed per identity with a per-client-IP fallback for unauthenticated requests.
| Route | Requests per 60s window |
|---|---|
/api/v1/evaluate |
3000 — the hot enforcement path, given a high ceiling so it is never the bottleneck |
/api/v1/auth/login |
20, per IP (pre-auth route) |
| Policy dry-run | 20 |
| Red team | 15 |
| Everything else | 300 |
/healthz, /readyz, /metrics |
never throttled |
Only the two trusted-proxy knobs are exposed as Helm values; the ceilings above are settable through
config.extraEnv
(NRVQ_HTTP_RATE_LIMIT_EVALUATE_PER_WINDOW and friends).
X-Forwarded-For is client-writable, so trusting its left-most entry would let a caller rotate the
header and never fill a bucket — a throttle bypass, worst on the pre-auth login route. Ignoring it
entirely would collapse every caller behind a proxy into one bucket, so a single abuser throttles
everyone. The header is believed only when both the TCP peer is a trusted proxy and the chain is
long enough.
config.httpRateLimitTrustedProxyCidrs(["127.0.0.0/8", "::1/128"]) — which peers may be believed. The default is exactly the in-pod nginx thatconfig.internalTlsruns in front of the API. Widen it only to proxies you operate: anything in this list can set any client’s rate-limit identity.config.httpRateLimitTrustedProxyHops(1) — the Nth entry from the right to use.1matches the shipped tls-proxy, which replaces the header (proxy_set_header X-Forwarded-For $remote_addr), leaving exactly one unforgeable entry. Raise it only if you add more appending proxies in front, and widen the CIDR list to match.0disables XFF entirely.
A workload that bypasses the proxy and hits the API port directly is not loopback, so its XFF is ignored and the forgeable path is unreachable for it.
In-process L1 cache
Section titled “In-process L1 cache”config.inprocCacheTtlS is an opt-in per-pod L1 cache on the enforcement hot path, in seconds. 0
(the default) disables it; the recommended production value is 5. Measured on a 2-node AKS cluster:
warm read p50 21.9ms → 3.2ms (−84%), floor 14.5ms → 1.4ms.
What it caches, precisely:
- namespace posture, the stored trust score, and the trust calculator’s history/profile reads — these use the TTL as-is, so this value is their convergence bound;
- the pre-override base policy decision — additionally clamped to the Redis eval TTL (5s) and cleared eagerly on any policy change, so it can never be staler than the Redis eval cache.
Never cached: the admin freeze (agent_frozen) and the trust cap (agent_trust_override). Those
are read fresh on every call, so an incident-response freeze takes effect on the very next call
regardless of this setting — the kill switch is not on the caching path.
What the TTL costs you: a posture or threshold change made from the console, and any trust-input change, is not observed by an already-warm pod until the entry expires. Enable deliberately.
The ConfigMap reads this key with dig, not default, so an explicit 0 genuinely disables the cache
rather than being treated as “unset” and silently re-enabled.
Data retention (config.retention.*)
Section titled “Data retention (config.retention.*)”One hourly background pruner enforces every window below. 0 (or negative) on any window disables
that window — keep forever. The console’s Settings page shows the effective values, read-only.
| Data type | Default | Helm value | What 0 means |
|---|---|---|---|
| Audit log | 30 days | auditRetentionDays |
keep forever |
| Compliance coverage trend points + export events | 30 days | coverageSnapshotRetentionDays |
keep forever |
| Asset-graph snapshots | newest 10 per namespace | graphSnapshotKeepPerNamespace |
keep all |
| Agent registry entries | removed 90 days after last_seen |
agentRegistryRetentionDays |
never removed |
| API keys (newly created) | 90-day expiry | apiKeyDefaultTtlDays |
new keys never expire |
| Policy-intent drafts | 14 days real / 24 h test-class / cap 50 per namespace | draftTtlDays, draftTtlTestHours, draftCapPerNamespace |
keep forever / uncapped |
| Drafts endpoint page size | 15 | draftsPageSize |
— |
| Policy versions | last 20, plus anything newer than 90 days | policyVersionKeepCount, policyVersionKeepDays |
keep forever |
| Red-team detail | newest 1 run, or any within 7 days | redteamDetailKeepRuns, redteamDetailKeepDays |
keep forever |
| Red-team summaries | newest 20 runs, or any within 30 days | redteamSummaryKeepRuns, redteamSummaryKeepDays |
keep forever |
| Red-team history page size | 20 | redteamHistoryPageSize |
— |
| Enforcing policies | never expire | — | — |
- Audit log — the console never displays a window longer than 30 days, which is what the default
matches. For SOC 2 / ISO evidence windows raise
auditRetentionDaysto 90–365, and use the audit-evidence export packs for durable point-in-time evidence regardless of the live window. - Asset-graph snapshots — readers only ever load the newest snapshot; keeping 10 per namespace just stops unbounded growth.
- Agent registry — an admin can remove one immediately with
DELETE /api/v1/agents/{spiffe_id}. - API keys — the 90-day default applies to newly created keys only, with a per-key override at creation (including never-expires). Pre-existing keys are untouched. Revocation is a soft revoke — revoked keys are kept, not deleted.
- Red team — the latest run per namespace is never pruned.
- Policy versions — pruning never touches the current enforcing version.
- Enforcing policies never expire, deliberately. An expiring security control would be silent un-protection, so policy expiry is not a retention knob.
Local auth (auth.*)
Section titled “Local auth (auth.*)”The primary no-IdP login path: username/password against a local, bcrypt-hashed account.
| Key | Default | What it does |
|---|---|---|
auth.enabled |
true |
Set false to disable local login entirely (SSO/CLI-only). |
auth.adminUsername |
admin |
Seeded admin username. |
auth.adminPassword |
norviq (sentinel) |
Left at the sentinel the chart auto-generates a strong random first password, persisted across upgrades. Retrieve it with kubectl get secret norviq-secrets -o jsonpath='{.data.NRVQ_AUTH_ADMIN_PASSWORD}' | base64 -d. You are forced to change it on first login either way. With config.requireStrongSecret: true the API refuses to start while the password is still the literal norviq. |
auth.sessionTtlSeconds |
3600 |
Session token TTL. |
auth.loginMaxAttempts / loginWindowSeconds |
5 / 300 |
Brute-force lockout: after N failed logins for a username within the window, further attempts 429 until it passes. |
auth.minPasswordLength |
12 |
Enforced on a new password at change time. |
auth.requireBoundAgentIdentity |
true |
Every non-admin caller must present a credential bound to an agent class, or the API returns 403. |
OIDC / SSO (oidc.*)
Section titled “OIDC / SSO (oidc.*)”Default-off; the API keeps validating legacy HS256 tokens until an IdP is wired in. Enabling adds
RS256/ES256 validation alongside HS256 — set legacyHs256Enabled: false at cutover.
| Key | Default | What it does |
|---|---|---|
oidc.enabled |
false |
Enabling requires issuer, jwksUrl and audience, or the render fails. |
oidc.issuer / oidc.audience |
"" |
Token iss / the API’s client-audience id. |
oidc.consoleClientId |
"" |
The public (browser/SPA) client id the console uses for Auth Code + PKCE. Register a public client with redirect URI <console>/auth/callback. When set, the UI renders “Sign in with SSO” with no rebuild — it is injected at runtime. |
oidc.providerName |
"" |
Human-readable IdP name shown in login copy, e.g. Okta. |
oidc.jwksUrl |
"" |
IdP JWKS endpoint. |
oidc.groupClaim |
groups |
Claim holding the user’s group list. |
oidc.legacyHs256Enabled |
true |
Keep validating local HS256 tokens alongside OIDC. |
oidc.groupMappings |
{} |
Map IdP groups to Norviq (role, namespace), e.g. {"norviq-admins":{"role":"admin"},"team-a":{"role":"viewer","namespace":"team-a"}}. |
RBAC (rbac.*)
Section titled “RBAC (rbac.*)”The chart ships the norviq-admin / norviq-policy-editor / norviq-viewer ClusterRoles but no
subject bindings. Map them to your IdP groups or ServiceAccounts with rbac.exampleBindings.enabled
(false) and rbac.bindings ([]). Each binding takes role (one of the three ClusterRoles), kind
(Group | User | ServiceAccount), name, and namespace for a ServiceAccount:
rbac: exampleBindings: enabled: true bindings: - role: norviq-admin kind: Group name: norviq-platform-admins - role: norviq-policy-editor kind: ServiceAccount name: ci-policy-bot namespace: norviqSIEM (siem.*)
Section titled “SIEM (siem.*)”| Key | Default | What it does |
|---|---|---|
siem.enabled |
false |
Outbound audit forwarder. |
siem.webhookUrl |
"" |
Where new audit rows are streamed. |
siem.format |
ndjson |
ndjson | syslog. |
siem.pollIntervalSeconds |
30 |
How often the API drains new rows to the webhook. |
The authenticated GET /api/v1/audit/export endpoint is always available regardless of this setting,
so a forwarder is a convenience, not the only export path.
Observability (otel.*)
Section titled “Observability (otel.*)”Prometheus metrics are always served at /metrics on the norviq-api Service port (api.port),
regardless of otel.enabled. There is no separate metrics port. The knobs below only control how your
Prometheus discovers that endpoint.
| Key | Default | What it does |
|---|---|---|
otel.enabled |
false |
OTLP trace export to a collector over gRPC. No collector ships with Norviq. |
otel.endpoint |
"" |
OTLP gRPC endpoint, e.g. http://otel-collector:4317. Required when otel.enabled — the render fails otherwise, so spans are never silently dropped into a void. |
otel.metrics.scrapeAnnotations |
false |
Add prometheus.io/scrape annotations to the norviq-api Service. |
otel.metrics.serviceMonitor.enabled |
false |
Render a Prometheus-Operator ServiceMonitor. Requires the monitoring.coreos.com CRDs. |
otel.metrics.serviceMonitor.interval / scrapeTimeout |
30s / 10s |
Scrape cadence. |
otel.metrics.serviceMonitor.additionalLabels |
{} |
Extra labels so your Prometheus’s serviceMonitorSelector picks this up, e.g. {release: kube-prometheus-stack}. |
Ingress (ingress.*)
Section titled “Ingress (ingress.*)”Off by default, following the Grafana/Istio convention: the out-of-the-box access path is
kubectl port-forward svc/norviq-ui. Because the console’s nginx already proxies /api/* and /ws/*
to norviq-api, a single host serves both the UI and the API — no second host, no path rewrite.
| Key | Default | What it does |
|---|---|---|
ingress.enabled |
false |
Render an Ingress for the console Service. |
ingress.className |
nginx |
ingressClassName — the controller that fulfils the Ingress. |
ingress.host |
norviq.example.com |
Hostname routed to the console. |
ingress.tls |
false |
Serve HTTPS. The chart deliberately does not generate an ingress certificate — a self-signed cert is useless for real HTTPS and hides a false “TLS works”. Set true and either pre-create the secret named in tlsSecretName or issue it via a cert-manager annotation; with tls: true and no such cert the ingress falls back to the controller’s default self-signed cert. |
ingress.tlsSecretName |
norviq-ingress-tls |
TLS Secret the Ingress terminates with. |
ingress.annotations |
{} |
Extra annotations — cert-manager.io/cluster-issuer, proxy body size, timeouts. |
Multi-cluster fleet (fleet.*)
Section titled “Multi-cluster fleet (fleet.*)”Read-only. Everything is off by default — a single-cluster install renders zero fleet resources and behaves exactly as a standalone install. The relay is fire-and-forget: a hub outage never affects local enforcement on any spoke.
Spoke side (fleet.enabled: false) runs an in-process relay pushing agent and audit rollups to the
hub.
| Key | Default |
|---|---|
fleet.clusterId / clusterName / region |
"" |
fleet.apiUrl |
"" — hub base URL the relay posts to |
fleet.relayIntervalSeconds |
60 |
fleet.pullIntervalSeconds |
60 — how often the spoke pulls, verifies and applies the signed policy bundle |
fleet.staleAfterSeconds |
180 — hub-side heartbeat staleness threshold |
fleet.residency |
false — keep raw audit in-cluster; rollups still leave, hub drill-down is blocked |
fleet.bundlePubkey |
"" — the fleet signing public key (trust root). Empty means the spoke applies no bundle, which is fail-closed, not fail-open |
fleet.oidc.tokenUrl / clientId / clientSecret |
"" — relay→hub auth, with an HS256 break-glass fallback if unset |
Two further spoke keys are read by the ConfigMap but are not listed in values.yaml, so they only
exist if you set them: fleet.clusterEndpoint (a console URL for hub deep-links) and fleet.labels
(a map rendered as NRVQ_FLEET_CLUSTER_LABELS). Both default to empty.
Hub side (fleet.hub.enabled: false) renders the fleet-api control plane plus a dedicated
fleet-postgresql, only on the cluster hosting the control plane.
| Key | Default |
|---|---|
fleet.hub.pgUrl |
postgresql://norviq:norviq_dev@fleet-postgresql:5432/norviq_fleet — fails the render with config.requireStrongSecret: true while it carries the shipped norviq_dev password |
fleet.hub.signingKey / signingKeySecretName |
"" — the fleet signing private key PEM, hub only, distinct from api.secretKey. Prefer the pre-created Secret over inlining |
fleet.hub.bundleTtlSeconds |
900 |
fleet.hub.replicas / storage |
1 / 5Gi |
fleet.hub.resources |
req 50m/128Mi, lim 500m/512Mi |
fleet.hub.pdb / autoscaling / spread |
all off (pdb.minAvailable 1; HPA 2–6 @ 70%) — values-prod.yaml turns them on when the hub is enabled (3 replicas, PDB minAvailable: 2, HPA 3–6, spread on) |
fleet.hub.postgresql.* |
database norviq_fleet, username norviq, password norviq_dev, port 5432, storage 5Gi, ha.enabled false (instances 3, serviceName fleet-postgresql-ha-rw), resources req 50m/128Mi, lim 500m/512Mi |