Skip to content

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

  • A Kubernetes cluster, 1.30+ — the chart declares kubeVersion: ">=1.30.0-0" and helm install refuses on anything older. AKS, EKS, GKE, kind, or any conformant distribution.
  • kubectl, with its current context pointed at that cluster
  • helm 3 (3.14+ if you want --reset-then-reuse-values, used in step 5)
Terminal window
kubectl config current-context # confirm you're pointed at the intended cluster
kubectl 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.

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:

Terminal window
kubectl create namespace chatbot-prod

The 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 a
ResourceQuota 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 common
first-install failure.)

Two details that are easy to get wrong from that message:

  • The baseline NrvqPolicy it mentions is rendered into the release namespace (norviq), one per listed namespace, each with spec.target.namespace pointing at the tenant. It is the ResourceQuota that lands in the tenant namespace. So look for the baselines with kubectl get nrvqpolicy -n norviq, not in chatbot-prod.
  • The preflight only runs against a live cluster. helm template and --dry-run=client have nothing to ask, so lookup returns 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.

Terminal window
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:

Terminal window
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.com
Installing 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):

Terminal window
git clone https://github.com/norviq-dev/norviq.git
cd 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:

  • Imagesimages.registry defaults to ghcr.io/norviq-dev/, so this pulls the public norviq-engine images (engine/api/ui/webhook/bootstrap) straight from GHCR. No registry login and no imagePullSecrets for a stock install. global.imageRegistry is 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: disable for the bundled single-node Postgres StatefulSet, which has no TLS listener, and require for CloudNativePG HA or any external/managed host. An explicit value always wins. Do not pass --set config.dbSslMode=disable on a managed database — you would be turning TLS off.
  • Nothing else to opt into. policyQuotaNamespaces is 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.

Terminal window
kubectl -n norviq rollout status deploy/norviq-api
kubectl -n norviq rollout status deploy/norviq-engine
kubectl -n norviq rollout status deploy/norviq-webhook
kubectl -n norviq rollout status deploy/norviq-ui

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

Terminal window
kubectl -n norviq port-forward svc/norviq-ui 8080:80

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

Terminal window
kubectl get secret norviq-secrets -n norviq -o jsonpath='{.data.NRVQ_AUTH_ADMIN_PASSWORD}' | base64 -d ; echo

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

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.

Terminal window
pip install norviq
norviq login -n norviq --console-url http://localhost:8080

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

Terminal window
helm upgrade norviq oci://ghcr.io/norviq-dev/charts/norviq --version 0.2.5 -n norviq \
--reset-then-reuse-values --set webhook.injection.enabled=true

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

Terminal window
kubectl label namespace chatbot-prod norviq-injection=enabled

The 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-class starts 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.failurePolicy defaults to Fail. 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. Set gateOnlyAgentPods=false for 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.

The repo ships ready-to-use examples under crds/examples/. Apply an agent class and the policy that targets it:

Terminal window
kubectl apply -f crds/examples/class-customer-support.yaml
kubectl apply -f crds/examples/policy-strict-chatbot.yaml

What these do:

  • class-customer-support.yaml (NrvqClass, cluster-scoped) — registers the customer-support agent class, with a descriptive tool list (allowedTools / blockedTools), maxCallsPerMinute: 60 and trust-score fields in its spec, so a NrvqPolicy can target it by name via target.agentClass. The tool-call decision comes from the policy below, not from this class spec.
  • policy-strict-chatbot.yaml (NrvqPolicy chatbot-strict, namespace chatbot-prod) — targets agentClass: customer-support, enforcementMode: block, preset: strict, priority: 200. The strict preset 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 — see webhook/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:

Terminal window
kubectl get nrvqpolicy -n chatbot-prod
# NAME TARGET MODE PHASE AGE
# chatbot-strict customer-support block Active 30s

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

Terminal window
kubectl get nrvqpolicy -n norviq
# baseline-cluster-guard-chatbot-prod <none> audit Active 6m

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:

Terminal window
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:

Terminal window
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:

Terminal window
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.

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_execution false-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 referencenorviq login, password recovery, policies, audit, agents, red-team, fleet