Skip to content

Configuration reference

This page documents the chart values you’re most likely to set. Every value has a sensible default; override with --set key=value or a values file (-f my-values.yaml). Defaults shown are the chart defaults.

The core is offline-first: every image is {registry}/{repository}:{tag}. Point registry at a private mirror and no step reaches the public internet.

Value Default Notes
registry ghcr.io/ultrvlt Prefix applied to all first-party images.
imageTag "" Global tag override. Empty → the chart’s appVersion (the release tag). A component’s own image.tag still wins.
imagePullPolicy IfNotPresent
imagePullSecrets [] Names of pre-created pull secrets (private mirror).

Provisioned as a ClickHouseInstallation (CHI) managed by the bundled operator. One replica by default; see Replicated storage for two or three.

Value Default Notes
clickhouse.database ultrvlt
clickhouse.user ultrvlt
clickhouse.password "" Empty → a random password is generated on first install and kept stable across upgrades.
clickhouse.leastPrivilege true The ingest gateway and the query API each connect to the database as their own user, with generated passwords kept in the same Secret. The ingest user can only add telemetry; the query API’s user can read and write only its own settings, with no rights to drop data or to read files or URLs from inside the database. clickhouse.user remains the administrator, used only for schema changes, backups, restores and retention changes. false → every component connects as clickhouse.user.
clickhouse.persistence.size 50Gi Size the PVC for your retention — see Sizing and capacity.
clickhouse.persistence.storageClass (cluster default)
clickhouse.resources 1 CPU / 2Gi req, 4Gi limit
clickhouse-operator.enabled true Set false if a cluster-wide ClickHouse operator already exists.

Two or three storage replicas, each holding all the data. Losing a node, a pod or a replica’s disk then loses nothing, and the portal keeps answering from the replicas that remain. Off by default. It adds three small coordination (Keeper) pods, which keep the replicas in step.

Value Default Notes
clickhouse.replication.enabled false Turn on replicated storage.
clickhouse.replicasCount 1 2 or 3 with replication. Above 1 without it is refused, since each replica would hold different data.
clickhouse.replication.migrate false Convert an existing install’s data (see below). Not needed for a new install.
clickhouse.replication.keeper.replicas 3 Coordination pods. Three keep working with one lost.
clickhouse.replication.keeper.persistence.size 2Gi
clickhouse.replication.keeper.resources 50m / 128Mi req, 512Mi limit

Replicas and coordination pods prefer separate nodes, so give the core at least as many nodes as storage replicas. Each node must be able to run the whole core by itself (see High availability).

A new install needs only the first two values:

clickhouse:
replicasCount: 2
replication:
enabled: true

An existing install keeps its data in an unreplicated database, and converting it is a deliberate step:

  1. Take a backup (Settings → Backups → Back up now), and wait for it to finish.
  2. Upgrade with replication.enabled: true and replicasCount: 2. The new pods start, and the upgrade stops at the schema step with a message asking for step 3. Nothing has changed: the portal and ingest keep working from the first replica.
  3. Upgrade again with replication.migrate: true as well as the values from step 2. (If you upgrade with --reuse-values, Helm reuses the values of the last successful upgrade, which is the one before step 2, so pass all three.) The data moves into a replicated database in place, which takes seconds and no extra disk. The other replicas then copy it from the first; on a large install that copy takes a while, and Core status shows the replicas catching up. Telemetry arriving meanwhile is kept.

The old database stays as ultrvlt_pre_replication, sharing its files with the new one, until you remove it. Once the replicas are in step and new data looks right, drop it:

Terminal window
kubectl exec -n <namespace> chi-<release>-ultrvlt-0-0-0 -c clickhouse -- \
clickhouse-client -q "DROP DATABASE ultrvlt_pre_replication"

To roll back instead, while it still exists (data received since the migration is lost):

Terminal window
# on every storage replica (chi-<release>-ultrvlt-0-0-0, -0-1-0, ...):
clickhouse-client -q "DROP DATABASE ultrvlt SYNC"
# then on the first one only:
clickhouse-client -q "RENAME DATABASE ultrvlt_pre_replication TO ultrvlt"

…and upgrade with replication.enabled: false and replicasCount: 1.

migrate can stay set afterwards: it only acts on an unreplicated database. Apart from that rollback, turning replication off again is not supported.

Adding or removing a replica is a change to replicasCount (between 2 and 3). A new replica copies the data from the others; a removed one is forgotten by the cluster once its pod has stopped.

If a replica was down for an upgrade, it catches up when it returns. A replica that has never been part of the cluster (for example one added while it couldn’t start) shows on Core status; run the upgrade again once it’s up.

Value Default Notes
schemaInit.enabled true Post-install/upgrade Job that applies the DDL (idempotent).
schemaInit.auditRetentionDays 365 Retention for the control-plane audit trail; re-tuned on each upgrade.

Limits on what a single portal query may use, so one very wide search can’t slow ingest or everyone else. Off by default; recommended for any shared production install — see Sizing and capacity.

Value Default Description
queryApi.queryLimits.maxExecutionTime "" (off) Stop a query after this long, e.g. 60s.
queryApi.queryLimits.maxMemoryUsage 0 (off) Bytes one query may use, e.g. 1073741824 (1 GiB, about a quarter of the default storage memory limit).
queryApi.queryLimits.maxRowsToRead 0 (off) Rows one query may scan.
queryApi.rateLimit.rps 0 (off) Sustained API requests per second, per user.
queryApi.rateLimit.burst 0 (= rps) Short bursts allowed above rps — a dashboard load fires many requests at once, so allow ~100.

A query that hits a limit fails with an error naming the limit, instead of degrading the whole system.

If storage can’t take writes for a while — restarting, being upgraded, or down — the ingest gateway keeps what it receives on disk and writes it, oldest first, as soon as storage is back. Nothing arriving during the outage is lost, up to the buffer’s limits. On by default.

Value Default Description
ingestGateway.spool.enabled true Buffer refused writes on disk. Off: they’re retried from memory for 5 minutes, then dropped.
ingestGateway.spool.maxSizeMB 1024 Buffer size per gateway replica. Past it, the oldest data is dropped.
ingestGateway.spool.maxAge 24h Data older than this is dropped instead of written.
ingestGateway.spool.existingClaim "" A PersistentVolumeClaim you create to hold the buffer (one gateway replica only).

By default the buffer lives on the pod’s own disk, so it survives the gateway restarting but not its pod being deleted or moved to another node. To keep it through that as well, create a claim (ReadWriteOnce is enough) and name it in existingClaim.

As a rough guide, 1 GiB holds about an hour of 1,000 records per second. While the buffer holds anything, and whenever it has had to drop data, it shows on Core status.

A per-site ceiling on how much telemetry the core accepts, so one site — a log storm, a misconfigured exporter — can’t crowd out every other site. Off by default.

Value Default Description
ingestGateway.quota.recordsPerSecond 0 (off) Records per second each site may send — spans, log lines and metric points together.
ingestGateway.quota.burstSeconds 10 How far a site may run ahead of its rate, in seconds of that rate.
ingestGateway.quota.perSite {} Per-site overrides, e.g. {edge-plant-7: 2000}. 0 exempts a site from the default.

A site over its quota is told to slow down rather than having data silently discarded:

  • A remote site keeps the refused telemetry in its on-disk buffer and resends it when the core accepts it again — it arrives late, not lost.
  • Sensors sending straight to the core (no remote-site connector) drop what is refused and log how much, once a minute.
  • Heartbeats are never throttled, so node counts, metering and “sensors up” stay correct while a site is over quota.

Throttled sites appear on Core status (under System), and the ingest.throttled_records metric — by site and signal — can drive a metric alert. The limit applies per ingest gateway replica: with three replicas, a site can reach up to three times the rate. For sizing a quota, see Sizing and capacity.

The checker that runs synthetic monitors from this cluster. On by default; with no monitors defined it does nothing.

Value Default Description
prober.enabled true Run the checker.
prober.location core The location name this cluster’s checks report as.
prober.otlpEndpoint "" Where results go. Empty means this cluster’s ingest gateway; with remote-site mTLS on, point it at a local site connector, as for the sensor.
prober.assignmentsEndpoint "" With remote-site mTLS on, also the local site connector: the checker then asks for its monitors through it and runs as that site.
prober.secretsSecret "" An existing Secret whose keys monitors may use as header values.
prober.egress.denyCIDRs [] Extra address ranges checks may not reach.
prober.egress.allowCIDRs [] Ranges to re-permit, overriding the built-in blocks (loopback, link-local and metadata, multicast) and denyCIDRs.
prober.networkPolicy.enabled false Restrict the checker’s network access to DNS, the core’s own services and egressCIDRs. Needs a CNI that enforces NetworkPolicy.
prober.networkPolicy.egressCIDRs [] Where checks may go when the NetworkPolicy is on.

The checker holds no database credentials and no Kubernetes API access.

Stores a share of ordinary requests instead of every one, to cut trace storage on busy clusters. Off by default — every span is kept.

Value Default Description
ingestGateway.spanSampling.keepOneIn 1 (off) Keep one in this many ordinary requests, e.g. 10 stores about 10%.
ingestGateway.spanSampling.slowMs 500 Requests at least this slow are always kept.
ingestGateway.spanSampling.rules [] Overrides for a service, a site, or both, each with its own keepOneIn and optional slowMs. The most specific rule wins.

What is always kept, whatever the rate:

  • Errors — failed requests, HTTP 5xx responses, database errors and failed DNS lookups. Error counts stay exact.
  • Slow requests — at or over slowMs, so the latency tail isn’t thinned.
  • Connection data and browser (RUM) events, which network, cost and RUM views add up rather than count.

Request rates, error rates, latency percentiles, SLOs and alerts stay exact. Before leaving a request out, the core counts it in a per-minute summary of every request it received, and the service pages, dashboards, alerts and SLOs read that summary (percentiles to within about 1%; latency SLO thresholds such as 100 ms, 250 ms or 1.5 s — two significant figures — are counted exactly). Sampling changes what you can open, not what you measure: the trace list shows the requests that were kept, so a given fast, successful request may not be there to open.

A few drill-down views — DNS names, database queries, dependencies and the error breakdown — count the stored requests, each standing for the ones it replaced. These are estimates, close for busy endpoints (on a test of 40,000 requests sampled one in ten, within 2% for counts and 3% for p95/p99) and rougher for rarely called ones.

The choice is made per trace, so every span of a multi-service trace is kept or dropped together — also with several gateway replicas. Rates that divide each other (say 5 and 10) keep a trace whole across services sampled at different rates.

ingestGateway:
spanSampling:
keepOneIn: 10 # ~10% of ordinary requests
slowMs: 500
rules:
- service: checkout # keep every checkout request
keepOneIn: 1
- site: edge-plant-7 # a busy site, sampled harder
keepOneIn: 20
slowMs: 1000

Core status shows, per site, how many spans arrived in the last hour and how many were left out.

queryApi.corsOrigins (default []) lists browser origins allowed to call the API from a different origin with a user’s session. Leave it empty when users reach the portal this chart serves — only set it if you host your own front end elsewhere.

Human authentication for the portal and read API. This gates browser access (distinct from the machine mTLS trust domain used by remote sites).

Value Default Notes
queryApi.auth.mode local none | local | oidc. none ships the API open — not recommended off a trusted network.
queryApi.auth.sessionTtl 12h
queryApi.auth.sessionSecret "" Cookie signing key. Empty → auto-generated on install and kept stable; every replica shares it. Set it explicitly if you render the chart without cluster access (helm template, Argo CD), or each render rotates it and signs everyone out.
queryApi.auth.existingSecret "" BYO Secret with session-secret, admin-username, and admin-password or admin-password-hash.
queryApi.auth.admin.username admin
queryApi.auth.admin.password "" Plaintext, hashed at boot. Provide this or passwordHash.
queryApi.auth.admin.passwordHash "" Pre-computed argon2id PHC (preferred — no plaintext at rest). Set hashed: true to use it.

In local mode the admin credential is required (fail-closed). In oidc mode it’s optional and stays available as a break-glass login.

Authorization Code + PKCE against a discovery-capable IdP. Each user’s groups and/or email address map to viewer / editor / admin, and sign-in can be limited to your email domains.

Value Notes
queryApi.auth.oidc.issuer IdP base URL serving /.well-known/openid-configuration.
queryApi.auth.oidc.clientId / clientSecret Client secret may instead come from existingSecret key oidc-client-secret.
queryApi.auth.oidc.redirectUrl e.g. https://ultrvlt.example.com/api/v1/auth/oidc/callback.
queryApi.auth.oidc.scopes Requested scopes. Empty → openid profile email groups. Set openid profile email for an IdP that has no groups scope (e.g. Google).
queryApi.auth.oidc.groupsClaim ID-token claim holding the user’s groups. Empty → groups.
queryApi.auth.oidc.adminGroups / editorGroups / viewerGroups Group → role mapping.
queryApi.auth.oidc.adminEmails / editorEmails / viewerEmails Email → role mapping, for an IdP that doesn’t send groups. Only a verified email matches.
queryApi.auth.oidc.defaultRole Role for users matching no group or email; empty → deny.
queryApi.auth.oidc.allowedDomains Only admit users whose verified email is in one of these domains. Empty → no restriction.

Admins can also grant roles by email from the portal’s Access page, with no redeploy — handy for adding a teammate. Those grants sit alongside the ones in config (which the portal shows but can’t change), take effect at the user’s next sign-in, and are recorded in the audit log.

A user gets the most privileged role that any of their groups, their email, or an Access-page grant gives them. Email-based roles and allowedDomains both rely on the identity provider marking the email as verified (email_verified); a provider that doesn’t is denied rather than trusted. Domains match exactly, so list each subdomain you want to admit.

Google sends no group membership at sign-in, so assign roles by email (or give everyone a defaultRole), and restrict sign-in to your Workspace domain:

queryApi:
auth:
mode: oidc
oidc:
issuer: https://accounts.google.com
clientId: <OAuth client ID>
clientSecret: <OAuth client secret>
redirectUrl: https://ultrvlt.example.com/api/v1/auth/oidc/callback
scopes: "openid profile email"
allowedDomains: [example.com]
adminEmails: [alice@example.com]
editorEmails: [bob@example.com, carol@example.com]
defaultRole: viewer

Create the OAuth client in Google Cloud Console as a Web application with the redirect URL above, and set its consent screen’s user type to Internal. With Google, allowedDomains also requires the account to belong to your Workspace organization, so a personal Google account registered with a work address is refused.

Connect a language model to add a written summary to each investigation. Off by default; nothing is sent anywhere until you set a provider and a model.

Value Default Notes
queryApi.ai.provider "" openai (any OpenAI-compatible API: vLLM, Ollama, LM Studio, Azure OpenAI, OpenAI), anthropic, gemini (with an API key), or vertex (Gemini on Google Cloud, no key; see below).
queryApi.ai.model "" The model name the provider expects, e.g. llama3.1:8b, claude-sonnet-4-5, gemini-3.5-flash.
queryApi.ai.baseUrl "" Where to send requests, e.g. http://ollama.ai.svc:11434/v1. Empty: the provider’s public API.
queryApi.ai.apiKey.existingSecret "" A Secret holding the API key (key api-key). Not needed for most self-hosted servers.
queryApi.ai.maxOutputTokens 1024 The longest summary the model may write.
queryApi.ai.gcpProject "" vertex only: the Google Cloud project. Empty: the project of the credentials.
queryApi.ai.gcpLocation "" vertex only: the region, e.g. europe-west4. Empty: global.
queryApi.ai.redactLogs false Leave log lines out of what is sent. Their counts are still included. Consider it for a model outside your network. Applies to AI agents too.

Gemini on Google Cloud (vertex) needs no API key. On GKE it uses the query API pod’s Workload Identity: bind the pod’s Kubernetes service account (queryApi.serviceAccount) to a Google service account that has the Vertex AI User role (roles/aiplatform.user) in the project.

Only an investigation’s evidence report is sent, never raw telemetry, and each summary is recorded in the audit log. A model in your own environment keeps the whole loop inside your network; small models (7–8B parameters) are enough, because the analysis is already done.

Let your team’s AI agents, such as Claude Code, Cursor or Copilot, look up telemetry read-only, with an API token. See AI agents.

Value Default Notes
queryApi.mcp.enabled false Serve the MCP endpoint at /api/v1/mcp. What agents look up goes to the models they use, so it’s off until you turn it on.
Value Default Notes
queryApi.license.existingSecret "" Name of a Secret with key valid.lic.
queryApi.license.content "" Inline .lic (chart creates the Secret).
queryApi.connected.enabled false Automatic license renewal (recommended). Enable with a portalUrl + tokenSecret. Leave off for a fully air-gapped install.
queryApi.connected.portalUrl "" The license endpoint you were given.
queryApi.connected.tokenSecret "" Secret name with key token.
queryApi.connected.callhomeInterval 6h

Connected mode is the recommended way to run — it keeps your license current for you and sends usage metadata only, never telemetry. Turn it on by setting connected.enabled, portalUrl, and tokenSecret. For offline environments, leave it off and manage the .lic yourself — see Licensing & connected mode.

Leave both license fields empty for an unlicensed boot (core runs UI-locked).

Alert notification channels store only a secret_key in the database — never a secret value. The core resolves ULTRVLT_NOTIFY_SECRET_<key> from the Secret below. Keys must be valid env-var suffixes ([A-Za-z0-9_]); that same string is what you type into a channel’s secret_key field.

Value Default Notes
queryApi.notify.secrets {} Inline map, e.g. { slack_hook: "https://hooks.slack.com/..." } (chart renders a Secret).
queryApi.notify.existingSecret "" BYO Secret; its keys become ULTRVLT_NOTIFY_SECRET_<key>.
queryApi.notify.allowPrivateTargets true Whether webhook and email targets may be private (RFC 1918 / ULA) addresses, such as an internal chat bridge. Set false if every target is on the internet.

Webhook/email targets that need no secret work without any of this. Whatever the setting, notifications are never sent to loopback, link-local or cloud-metadata addresses, and webhook URLs must be http:// or https://.

Value Default Notes
portalUi.ingress.enabled false
portalUi.ingress.className ""
portalUi.ingress.host ultrvlt.local
portalUi.ingress.annotations {} e.g. ALB/ACM annotations.
portalUi.ingress.tls [] e.g. [{secretName: ultrvlt-tls, hosts: [ultrvlt.example.com]}].

Serve the portal over HTTPS — the session cookie is Secure.

If the portal sits behind a load balancer or ingress controller, list its address range in queryApi.trustedProxies (for example ["10.0.0.0/8"], or single addresses). Otherwise every user appears to come from the proxy’s address — the audit log records the proxy, and failed-login lockouts and rate limits treat all users as one. With the range listed, the user’s own address is read from X-Forwarded-For, skipping only your proxies, so a client can’t choose its own. Leave it empty when users reach the portal directly.

Opt-in. Enabling enrollment lets one core take in telemetry from other clusters (sites), each authenticated by a certificate the core itself issues. It stands up the enrollment authority, turns on the portal’s Add site wizard, and switches the ingest endpoint to mutual TLS. For the full operator walkthrough, see Connecting remote sites.

Value Default Notes
enrollment.enabled false Turns on the enrollment authority + join-token service, enables the Add site wizard, and puts the ingest endpoint into mutual-TLS mode.
enrollment.certTtlHours 2160 (90d) Lifetime of the site certificates the core issues; sites auto-renew well before expiry.
enrollment.adminKey "" Key protecting the enrollment admin API. Empty → generated on first install and kept stable.
enrollment.existingSecret "" BYO Secret with key admin-key instead of the generated one.
enrollment.persistence.size 1Gi Holds the enrollment authority’s key material and issued-token record — keep it.

The ingest endpoint is in-cluster by default. To let remote sites reach it, publish it on a connection-level (L4) load balancer — the core still terminates the mutually-authenticated session itself, so the balancer only forwards it.

Value Default Notes
ingestGateway.wan.enabled false Publish the ingest endpoint on an external load balancer for remote sites.
ingestGateway.wan.annotations {} Provider load-balancer annotations (e.g. an AWS NLB — see the deploy guide).
ingestGateway.wan.sourceRanges [] CIDRs allowed to reach it — lock this to your sites’ egress addresses. Empty → open (mutual TLS still gates ingest, but scope the network too).
ingestGateway.wan.port (ingest port) Override the published port if you need to.
ingestGateway.mtls.serverNames ingest-gateway The name sites verify on the core’s certificate. Keep in sync with queryApi.enroll.coreServerName.

Set these to the public addresses remote sites dial; the Add site wizard bakes them into the ready-to-run connector command it generates, so operators don’t have to assemble them by hand.

Value Default Notes
queryApi.enroll.publicUrl "" Public URL a site uses to enroll, e.g. https://enroll.ultrvlt.example.com.
queryApi.enroll.corePublicEndpoint "" Public host:port of the ingest endpoint, e.g. ingest.ultrvlt.example.com:4317.
queryApi.enroll.coreServerName ingest-gateway Name the site verifies on the core’s certificate; must match ingestGateway.mtls.serverNames.

Off by default; only works on BTF-enabled Linux kernels. Validate the node kernel first (see Deploy to EKS).

Value Default Notes
sensor.enabled false
sensor.siteId / sensor.clusterId default Tag captured data.
sensor.privileged false false runs the sensor with only what it needs: Linux capabilities BPF, PERFMON, SYS_PTRACE, SYS_ADMIN and SYSLOG (to name kernel functions in CPU profiles), every other capability dropped, the runtime’s default seccomp profile, and AppArmor unconfined (so it can read other processes’ details to capture their encrypted traffic). It captures the same as a privileged container. true runs it privileged — use that on SELinux-enforcing hosts such as Bottlerocket or OpenShift, or if the sensor logs attach_failed or /proc access denied warnings.
sensor.profiling.enabled true Continuous CPU profiling: every CPU on the node is sampled at a low fixed rate and each workload’s stacks are kept for 7 days as flame graphs (APM → Profiles). Costs well under 1% of a CPU and around 60 MiB of the sensor’s memory. Python processes are named at the Python level by reading their interpreter’s frames, which is why the sensor reads other processes’ memory (SYS_PTRACE).
sensor.profiling.hz 19 Samples per second per CPU. Higher gives more detail for short windows, at proportionally more cost.
sensor.scrubSqlLiterals true Replaces the values in captured Postgres and MySQL queries, and in database error messages, with ? — WHERE email = ? rather than the address itself. false stores statements exactly as sent, readable by anyone who can view traces. Same value in the remote-site chart.
sensor.tolerations [{operator: Exists}] Runs on every node, including control-plane.
Value Default Notes
clusterState.enabled false Singleton Deployment; reads k8s object state for the Infrastructure pages (no eBPF).

Opt-in OpenTelemetry Collector DaemonSet that forwards pod logs as OTLP. Not a first-party image — pinned explicitly.

Value Default Notes
logCollector.enabled false
logCollector.image.tag 0.111.0 Pinned to a statically-linked contrib build; re-verify on bumps.
logCollector.siteId / clusterId default Keep in sync with the sensor’s.
logCollector.k8sAttributes true Resolve service.name from the k8s workload so log service matches span service.
logCollector.excludeLogPaths [] Extra pod-log globs to drop.

Opt-in receivers that let existing telemetry tools deliver into the core with no custom glue. Both are off by default and are pure receivers, so they work connected and air-gapped. When remote-site enrollment is on, each reuses the WAN server certificate and requires a client certificate, so an external sender is authenticated just like an enrolled site.

Value Default Notes
ingestGateway.promRemoteWrite.enabled false Prometheus remote-write endpoint (POST /api/v1/write) into the metrics path. See Prometheus remote-write.
ingestGateway.promRemoteWrite.port 19291 Port Prometheus sends to.
ingestGateway.otlpHttp.enabled false OTLP/HTTP endpoints (/v1/logs, /v1/metrics, /v1/traces) for senders that speak OTLP/HTTP rather than gRPC — e.g. Fluent Bit forwarding logs. See Fluent Bit & Fluentd.
ingestGateway.otlpHttp.port 4318 Conventional OTLP/HTTP port.

Browser telemetry and session replay arrive on the OTLP/HTTP listener, so ingestGateway.otlpHttp.enabled must be on. Because beacons come from untrusted browsers over the public internet, the browser-facing path is gated by an origin allowlist and optional abuse controls; server-to-server telemetry (no Origin header) is unaffected. See Browser monitoring.

Value Default Notes
ingestGateway.rum.allowOrigins [] Web origins permitted to send browser telemetry (CORS). Empty → browser ingest is blocked.
ingestGateway.rum.ingestKey "" Shared key the SDK presents on every browser upload (init({ ingestKey })). A throttling credential, not a strong secret; authenticates but binds no site; empty → not required.
ingestGateway.rum.siteKeys [] Per-site ingest keys — a list of { site, key }. A browser presenting a listed key has its data tagged with that key’s site server-side, overriding the page’s self-asserted siteId, so the UI site scope separates data reliably (spoof-resistant, like an mTLS site). Each key must be unique.
ingestGateway.rum.ratePerSec 0 Per-origin rate limit on browser uploads. 0 → disabled.
ingestGateway.rum.rateBurst 0 Burst allowance; 0 → defaults to ratePerSec.

Session replay needs a blob store for the recordings. Cloud object stores are the recommended multi-pod path; the file:// backend is single-node/dev and needs a ReadWriteMany volume shared by both the gateway and the read API.

Value Default Notes
blobStore.enabled false Turns on the session-replay chunk store.
blobStore.url file:///var/lib/ultrvlt/rum-blobs Storage URL; the scheme picks the backend (file://, s3://, gs://, azblob://).
blobStore.retentionDays 30 Daily GC removes recordings older than this. Keep ≥ how long sessions stay queryable. 0 → keep forever.
blobStore.persistence (off) PVC for the file:// backend only; mountPath must equal the file:// URL’s path and the class must support ReadWriteMany.

Running RUM in production (cloud storage + a public ingress)

Section titled “Running RUM in production (cloud storage + a public ingress)”

For a multi-pod install, back replay on a cloud object store and give the pods a cloud identity to reach it. The gateway is the writer and query-api is the reader; the browser never gets storage credentials. On AWS that’s an S3 bucket plus IRSA — annotate each ServiceAccount with an IAM role ARN.

Value Default Notes
ingestGateway.serviceAccount.create false Create a ServiceAccount for the gateway (the replay writer).
ingestGateway.serviceAccount.name "" Empty + create → <release>-ingest-gateway.
ingestGateway.serviceAccount.annotations {} e.g. eks.amazonaws.com/role-arn: <writer-role> for IRSA.
queryApi.serviceAccount.create false Create a ServiceAccount for query-api (the replay reader).
queryApi.serviceAccount.name "" Empty + create → <release>-query-api.
queryApi.serviceAccount.annotations {} e.g. eks.amazonaws.com/role-arn: <reader-role> (read-only is enough).

Browser beacons need a public HTTPS route to the OTLP/HTTP listener. Keep it a separate surface from the portal — RUM ingest is internet-open, guarded by the gateway’s own origin allowlist + per-site keys + rate limit, whereas the portal is an admin console you’ll usually restrict. Requires ingestGateway.otlpHttp.enabled.

Value Default Notes
ingestGateway.ingress.enabled false Browser-facing HTTPS Ingress to the gateway :4318.
ingestGateway.ingress.className "" e.g. alb or nginx.
ingestGateway.ingress.host rum.ultrvlt.local A dedicated hostname for ingest.
ingestGateway.ingress.annotations {} e.g. ALB/ACM annotations (terminate TLS, redirect HTTP→HTTPS).
ingestGateway.ingress.tls [] For in-cluster TLS (nginx + cert-manager). Omit when the LB terminates TLS with ACM.
ingestGateway.ingress.paths [{path: /v1/, pathType: Prefix}] Routed to the gateway; the default prefix covers /v1/traces, /v1/metrics, /v1/logs, /v1/rum/replay.

On AWS, the bucket + both IRSA roles can be provisioned with the reusable Terraform module under deploy/aws/rum/ — its outputs are the role ARNs and the s3://… URL you set above. Keep blobStore.retentionDays ≤ the bucket’s lifecycle expiry so S3 never removes a recording the app still expects to serve.

Scheduled and on-demand ClickHouse backups to a dedicated backup disk (the BACKUP statement carries no storage credentials).

Value Default Notes
backup.enabled false
backup.schedule 0 2 * * * Daily at 02:00 UTC.
backup.destination.type file Dedicated PVC. S3/GCS/Azure land later.
backup.destination.persistence.size 50Gi Use a different storageClass/AZ than the data volume.
backup.prune.enabled false Sidecar prune; keeps a snapshot if EITHER keepLast or maxAgeDays still protects it.

Opt-in singleton that tracks a signed release channel and serves upgrade status to the portal’s Updates page.

Value Default Notes
lifecycleManager.enabled false
lifecycleManager.channel stable stable (recommended) or latest.
lifecycleManager.channelsRepo ghcr.io/ultrvlt/charts/ultrvlt-channels
lifecycleManager.networkPolicy.enabled false Only the query API’s pods may connect to the lifecycle manager. Needs a cluster network that enforces NetworkPolicy.
lifecycleManager.upgrades.enabled false Opt-in actuation — adds a verified “Deploy now” button via a privilege-separated Job.
lifecycleManager.upgrades.schedule.enabled false Opt-in automatic weekly-window upgrades (requires upgrades.enabled).