Get started
This walkthrough takes you from an existing Kubernetes cluster to a policy that actually blocks a
tool call — create the tenant namespace, install the chart, sign in, apply a policy, and watch a
decision flip from allow to block.
New to Norviq? Start with What is Norviq and Concepts: Norviq is a policy
enforcement point (PEP) that sits between an agent and its tools, evaluates every call against
OPA/Rego policies scoped to the workload’s identity, and returns allow / block / escalate /
audit.
Everything below is pinned to chart 0.2.5 (appVersion 0.2.5).
1. Prerequisites
Section titled “1. Prerequisites”- A Kubernetes cluster, 1.30+ — the chart declares
kubeVersion: ">=1.30.0-0"andhelm installrefuses on anything older. AKS, EKS, GKE, kind, or any conformant distribution. kubectl, with its current context pointed at that clusterhelm3 (3.14+ if you want--reset-then-reuse-values, used in step 5)
kubectl config current-context # confirm you're pointed at the intended clusterkubectl version # confirm the server is 1.30+A single-node cluster is enough to evaluate the enforcement path end-to-end. Multi-node HA and the multi-cluster fleet need a multi-node cluster — see Deployment.
2. Create the tenant namespaces first
Section titled “2. Create the tenant namespaces first”Create your tenant namespaces — the ones that will run agent workloads — before you install.
Every namespace you list in policyQuotaNamespaces gets a ResourceQuota rendered directly into it,
and a ResourceQuota is a namespaced object, so a namespace that does not exist yet fails the
install:
kubectl create namespace chatbot-prodThe chart preflights this with a lookup and fails with the fix in the message, rather than dying
partway through on namespaces "chatbot-prod" not found and leaving the release in failed:
policyQuotaNamespaces lists 1 namespace(s) that do not exist: chatbot-prod. Each one gets aResourceQuota and a baseline policy, both namespaced, so they must be created BEFORE installing:
kubectl create namespace chatbot-prod
Then re-run the install. (Listing a namespace you have not created yet is the most commonfirst-install failure.)Two details that are easy to get wrong from that message:
- The baseline
NrvqPolicyit mentions is rendered into the release namespace (norviq), one per listed namespace, each withspec.target.namespacepointing at the tenant. It is theResourceQuotathat lands in the tenant namespace. So look for the baselines withkubectl get nrvqpolicy -n norviq, not inchatbot-prod. - The preflight only runs against a live cluster.
helm templateand--dry-run=clienthave nothing to ask, solookupreturns empty there and the check stays silent by design — it never reports a missing namespace it cannot actually see.
You do not need to create the norviq control-plane namespace by hand — --create-namespace
below does it. The chart does not template a Namespace object.
3. Install
Section titled “3. Install”helm install norviq oci://ghcr.io/norviq-dev/charts/norviq --version 0.2.5 \ -n norviq --create-namespace \ --set 'policyQuotaNamespaces={chatbot-prod}'The CRDs (NrvqPolicy, NrvqClass, NrvqConfig) ship inside the chart under helm/norviq/crds/,
so Helm installs them for you on first install. There is nothing to kubectl apply beforehand.
The published chart is cosign-signed, and the release workflow stamps every Norviq image reference to
an immutable sha256 digest at package time, so a pinned --version deploys exactly the bytes that
release built. Verify the signature first if you want to — this is the same command the release
pipeline runs against its own artefact before publishing:
cosign verify ghcr.io/norviq-dev/charts/norviq:0.2.5 \ --certificate-identity-regexp '^https://github.com/norviq-dev/norviq/.github/workflows/release.yml@.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comInstalling from a clone instead (contributors, or a modified chart)
From a clone, apply the CRDs first, from helm/norviq/crds/ (there is no crds/ directory of CRDs
at the repo root; crds/examples/ holds example CRs only):
git clone https://github.com/norviq-dev/norviq.gitcd norviq
kubectl apply -f helm/norviq/crds/kubectl create namespace chatbot-prod
helm install norviq ./helm/norviq -n norviq --create-namespace \ --set 'policyQuotaNamespaces={chatbot-prod}'This tracks whatever is on your checkout — which is what you want when changing the chart, and what
you do not want when evaluating a release. In particular, the in-tree values.yaml carries
floating -latest image tags with empty digest fields; only the packaged, published chart is
digest-pinned.
A few things worth knowing about this install:
- Images —
images.registrydefaults toghcr.io/norviq-dev/, so this pulls the publicnorviq-engineimages (engine/api/ui/webhook/bootstrap) straight from GHCR. No registry login and noimagePullSecretsfor a stock install.global.imageRegistryis the separate knob for mirroring the third-party images (OPA, Redis, Postgres, the nginx TLS proxy) in an air-gap. - Bundled dependencies — the chart deploys single-replica PostgreSQL (
postgres:16-alpine) and Redis (redis:7-alpine) StatefulSets, plus an OPA (openpolicyagent/opa:1.19.1-static) sidecar in every API and engine pod. Nothing external is required. The Postgres and Redis passwords are generated on first install and reused on every upgrade — there is nothing to set. config.dbSslMode— left empty (the default) it is derived from the datastore you chose:disablefor the bundled single-node Postgres StatefulSet, which has no TLS listener, andrequirefor CloudNativePG HA or any external/managed host. An explicit value always wins. Do not pass--set config.dbSslMode=disableon a managed database — you would be turning TLS off.- Nothing else to opt into.
policyQuotaNamespacesis the only override a basic install needs.
Wait for all four control-plane rollouts. The webhook matters as much as the API: it hosts the CRD
controller that syncs your policies to the API, and it is the component that ships the bundled Rego
presets (/app/presets) and materializes them into the database.
kubectl -n norviq rollout status deploy/norviq-apikubectl -n norviq rollout status deploy/norviq-enginekubectl -n norviq rollout status deploy/norviq-webhookkubectl -n norviq rollout status deploy/norviq-ui4. Reach the console and change the admin password
Section titled “4. Reach the console and change the admin password”The console’s nginx proxies /api/, /ws/, /healthz and /readyz to norviq-api:8080, so
whichever access method you pick below covers everything in this guide.
Ingress (the enterprise path). On any shared or production cluster, expose the norviq-ui
Service through an ingress controller and reach the console at your own hostname. The chart ships an
Ingress you can turn on (ingress.enabled, ingress.host, ingress.className, ingress.tls), or
you can front the Service yourself. TLS is bring-your-own — the chart deliberately does not issue an
ingress certificate; pre-create ingress.tlsSecretName (default norviq-ingress-tls) or issue one
via a cert-manager annotation in ingress.annotations. See Deployment and
Configuration.
Port-forward (quick dev/eval fallback).
kubectl -n norviq port-forward svc/norviq-ui 8080:80Open http://localhost:8080. The rest of this guide uses http://localhost:8080; substitute your
ingress host in production.
The chart seeds a local admin account (auth.adminUsername, default admin). Leaving
auth.adminPassword at its shipped sentinel value (norviq) makes the chart auto-generate a
strong random first password on install instead of using the literal default, and preserves it
across helm upgrade. Read it out of the norviq-secrets Secret:
kubectl get secret norviq-secrets -n norviq -o jsonpath='{.data.NRVQ_AUTH_ADMIN_PASSWORD}' | base64 -d ; echoSign in as admin with that password. You are forced to change it before you can do anything
else: while the token carries must_change, the API rejects every authenticated call except exactly
/api/v1/auth/change-password, /api/v1/auth/logout and /api/v1/me — matched by exact path, not
by suffix. The new password must be at least auth.minPasswordLength characters (12 by default),
must differ from the current one, and cannot be the chart’s default password.
POST /api/v1/auth/change-password revokes the token you used to make the request and returns a
fresh access_token with must_change cleared, so the console swaps tokens in place and you are not
signed out. Sessions on other devices are not revoked — the denylist is keyed by the presented
token’s hash.
Or sign in with the CLI (no password)
Section titled “Or sign in with the CLI (no password)”Install the norviq CLI and mint a first-login token straight from the cluster — useful on a fresh
install or in CI. The signing key never leaves the API pod.
pip install norviqnorviq login -n norviq --console-url http://localhost:8080norviq login runs the token minter inside the API pod (kubectl -n norviq exec deploy/norviq-api -c api -- python -m norviq.api.token_mint --ttl 3600) and prints a …/login#access_token=… deep link —
open it to sign in with no password, or paste the printed token into the console login’s token field.
Every command is in the CLI reference.
5. Enable sidecar injection for an agent namespace
Section titled “5. Enable sidecar injection for an agent namespace”Sidecar injection is off by default (webhook.injection.enabled: false). Turn it on:
helm upgrade norviq oci://ghcr.io/norviq-dev/charts/norviq --version 0.2.5 -n norviq \ --reset-then-reuse-values --set webhook.injection.enabled=trueThis renders the MutatingWebhookConfiguration and a one-shot Job that self-signs a TLS cert for the
webhook (no cert-manager required). Then opt the namespace in — the webhook’s namespace selector
matches the key norviq-injection=enabled (webhook/config.go’s NRVQ_ENABLE_LABEL, default
norviq-injection):
kubectl label namespace chatbot-prod norviq-injection=enabledThe same selector explicitly excludes kube-system, kube-public, kube-node-lease and the Norviq
release namespace, so Norviq can never gate its own control plane or the cluster’s.
That is only half the contract. Under the shipped default
(webhook.injection.gateOnlyAgentPods: true) a pod is routed to the injector only if it also
carries the label norviq.io/agent-class: <class>:
flowchart LR
A[Pod created] --> B{Namespace has<br/>norviq-injection=enabled?}
B -- no --> Z[Not injected — ungoverned]
B -- yes --> C{Pod has<br/>norviq.io/agent-class?}
C -- no --> Z
C -- yes --> D{Pod opted out?<br/>norviq-injection=disabled<br/>norviq.io/skip-injection=true}
D -- yes --> Z
D -- no --> E[Sidecar injected — every tool call evaluated]
Three consequences worth internalizing:
-
A pod without
norviq.io/agent-classstarts fine and runs ungoverned, silently. Nothing logs an error and nothing in the console computes injected-vs-expected. Verify by hand:Terminal window kubectl get pods -n chatbot-prod -L norviq.io/agent-class # blanks are NOT injected -
The gate exists because
webhook.injection.failurePolicydefaults toFail. Routing every pod in the namespace through the webhook would make a Norviq outage a precondition for starting that namespace’s database and ingress too. SetgateOnlyAgentPods=falsefor the older namespace-wide behaviour. -
The webhook rule is CREATE-only. Labelling a namespace does nothing to pods already running; injection happens as they are recreated.
The per-pod opt-out (norviq-injection=disabled, or the annotation norviq.io/skip-injection=true)
is honoured by default. Set webhook.injection.allowPodOptOut=false if a pod author must not be able
to remove their own governance.
The same norviq.io/agent-class value is what class-tier policy matches on, so use the value your
NrvqPolicy targets. By default the sidecar runs in sidecarMode: proxy: it POSTs each tool call to
the central API’s /api/v1/evaluate over mTLS with a namespace-scoped service token — nothing is
evaluated per-pod.
Check it worked: kubectl -n chatbot-prod get pod -l norviq.io/agent-class should show 2/2
containers, not 1/1.
The full injection model — modes, opt-out, credential delivery, and a clean teardown order — is in Sidecar injection.
6. Apply your first policy
Section titled “6. Apply your first policy”The repo ships ready-to-use examples under crds/examples/. Apply an agent class and the policy
that targets it:
kubectl apply -f crds/examples/class-customer-support.yamlkubectl apply -f crds/examples/policy-strict-chatbot.yamlWhat these do:
class-customer-support.yaml(NrvqClass, cluster-scoped) — registers thecustomer-supportagent class, with a descriptive tool list (allowedTools/blockedTools),maxCallsPerMinute: 60and trust-score fields in its spec, so aNrvqPolicycan target it by name viatarget.agentClass. The tool-call decision comes from the policy below, not from this class spec.policy-strict-chatbot.yaml(NrvqPolicychatbot-strict, namespacechatbot-prod) — targetsagentClass: customer-support,enforcementMode: block,preset: strict,priority: 200. Thestrictpreset blocks high-risk tool calls outright (execute_sql, anything whose name starts with or contains a destructive token —delete,drop,truncate,destroy,wipe,purge,erase) plus prompt-injection, SQL/shell-injection, PII/PCI, SSRF and secret-egress patterns in the params — seewebhook/presets/strict.rego.
The webhook’s CRD controller watches these objects and syncs them to the API
(POST /api/v1/policies) automatically — nothing else to run. Confirm they synced:
kubectl get nrvqpolicy -n chatbot-prod# NAME TARGET MODE PHASE AGE# chatbot-strict customer-support block Active 30sPHASE is blank until the controller syncs the policy, then moves to Active (or Error). The
chart’s own baselines live in the release namespace, targeting each tenant by spec.target.namespace:
kubectl get nrvqpolicy -n norviq# baseline-cluster-guard-chatbot-prod <none> audit Active 6mSee a decision flip
Section titled “See a decision flip”You can watch this from the console’s audit log, or drive it directly against
POST /api/v1/evaluate. The request shape is EvaluateRequest in
norviq/api/routers/evaluate.py: tool_name and agent_identity are required, and tool_params
defaults to {} if omitted, where the identity must carry at least spiffe_id and namespace
(AgentIdentity in norviq/sdk/core/events.py). A malformed identity is a 422, not a 500. The
response carries decision, rule_id, trust_score and reason.
The namespace in agent_identity is authorization-checked against the caller’s token and then
rewritten from it — an admin token can evaluate any namespace, a namespace-scoped token cannot
evaluate someone else’s. agent_class, spiffe_id and workload are bound from the credential the
same way, so the body cannot pick a looser class than the caller is entitled to.
Get a session token, using the password you set in step 4:
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"<your new password>"}' | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')A call the strict preset blocks:
curl -s -X POST http://localhost:8080/api/v1/evaluate \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{ "tool_name": "execute_sql", "tool_params": {"query": "SELECT * FROM orders"}, "agent_identity": { "spiffe_id": "spiffe://norviq/ns/chatbot-prod/sa/chatbot-agent", "namespace": "chatbot-prod", "agent_class": "customer-support" } }'# {"decision":"block","rule_id":"strict_default_block","trust_score":...,"reason":"Strict baseline blocked a high-risk tool"}The same shape with an allowed tool:
curl -s -X POST http://localhost:8080/api/v1/evaluate \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{ "tool_name": "search_kb", "tool_params": {"query": "refund policy"}, "agent_identity": { "spiffe_id": "spiffe://norviq/ns/chatbot-prod/sa/chatbot-agent", "namespace": "chatbot-prod", "agent_class": "customer-support" } }'# {"decision":"allow","rule_id":"default_allow","trust_score":...,"reason":"Allowed"}Same identity, same endpoint — only the tool call changed, and the decision flipped. Do not send
"framework": "redteam" on real traffic: that string is the product’s marker for fabricated
events, and rows carrying it are excluded from the Overview KPIs, compliance evidence and the audit
log’s default “real traffic only” filter. It defaults to empty, which is what you want here.
The class policy enforces even though the chart’s namespace baseline is in audit mode: audit-mode
layers are partitioned out of the priority contest and re-applied as tighten-only observers, so an
observing layer can raise an allow to audit but can never disarm a policy that enforces. Every
one of these calls is written to the audit log, which the console streams live.
7. Next steps
Section titled “7. Next steps”You’ve proven the engine end-to-end with curl. To protect a real agent — define its class,
apply policy, label its namespace and pods, and confirm enforcement — follow the worked example. For
a production rollout (managed Postgres/Redis, HA, ingress instead of port-forward), go to
Deployment.
- Example — protect an agent end-to-end — the full flow on a real workload
- How it works — the request path from tool call to decision
- Sidecar injection — the mutating webhook, injection modes, teardown order
- SDK integration — LangChain, LangGraph, CrewAI, AutoGen, Semantic Kernel
- Deployment — production HA, cloud (AKS / EKS / GKE), ingress, and multi-cluster fleet
- Concepts — agent classes, policy tiers, enforcement modes, SPIFFE identity
- Writing policies — authoring Rego, the intent generator, red-team
- Baseline controls — the 21 shipped detectors, including the
deny_shell_executionfalse-positive shape mentioned in step 3, and how to promote one from Monitor to Enforce - Tool registry — what tools Norviq knows about in a namespace and how
- MCP servers — governing Model Context Protocol traffic specifically
- Asset & attack graphs — see real reach, simulate kill chains, defend the gaps
- Compliance & coverage — MITRE ATLAS / OWASP LLM coverage, gaps, evidence pack
- Red team — the 34-attack catalog and how proven-blocking is computed
- Troubleshooting — health states, admission refusals, where the audit rows actually live, for when something above doesn’t go as shown
- CLI reference —
norviq login, password recovery, policies, audit, agents, red-team, fleet