Service-mesh mTLS¶
This covers the chart's in-cluster (service-mesh) mTLS — separate from and
independent of your edge/browser-facing TLS certificate (tls.mode,
tls.dnsName; see INSTALL.md).
It encrypts and mutually authenticates traffic inside the cluster — every
service-to-service call, plus the connections to Postgres and NATS — so that a
compromised pod or a network tap inside the cluster cannot read or impersonate
the platform's internal traffic.
If your edge certificate itself is issued by an internal/corporate CA (rather than a public one), that's a different gap — a pod calling back out to your edge hostname won't trust it either, since pods don't inherit the node's OS trust store. See Running with an internal CA for that case.
This is ON by default. A stock helm install ships encrypted internal
transport. That means the two prerequisites below are required, not
optional. The chart validates cert-manager's presence and fails the install
fast with a clear message if its CRDs are absent, rather than failing
mysteriously mid-apply.
Opting out (only for a cluster that genuinely cannot provide the prerequisites): set all three toggles to
false—serviceMtls.enabled,datastores.postgres.tls.enabled, anddatastores.nats.tls.enabled. The two datastore toggles requireserviceMtls.enabled, so a partial opt-out (e.g. serviceMtls off but a datastore toggle left on) is rejected at render time. With all three off, the chart needs neither cert-manager nor Reloader for the mesh.
Prerequisites (cluster infrastructure — the chart does not install these)¶
- cert-manager — issues the per-service certs. See the "Evaluate it" / "Run it" prerequisites in INSTALL.md.
- Stakater Reloader — restarts pods when their auto-rotated cert Secret
changes, so a renewal doesn't require a manual rollout:
helm repo add stakater https://stakater.github.io/stakater-charts helm install reloader stakater/reloader \ --namespace reloader --create-namespaceIf your cluster enforces the
restrictedPod Security Standard, the Reloader chart's default container securityContext is rejected and its pod never starts (FailedCreate/ 0 replicas). Add the two required fields:Verify Reloader is actuallyhelm install reloader stakater/reloader \ --namespace reloader --create-namespace \ --set reloader.deployment.containerSecurityContext.allowPrivilegeEscalation=false \ --set "reloader.deployment.containerSecurityContext.capabilities.drop={ALL}"1/1 Runningbefore enabling mTLS — if it isn't, certs rotate but pods never restart and mTLS breaks at the ~60-day renewal boundary.
How the trust works¶
By default the chart provisions a self-signed Platform CA (a cert-manager
Issuer + a 10-year CA cert) and issues every service a 90-day cert from it
(rotationPolicy: Always, renewed 30 days out). This CA is private to the mesh
— it is unrelated to your edge/browser certificate. The three data-plane
toggles are decoupled so you can roll them out in stages:
serviceMtls:
enabled: true # HTTP service-to-service mTLS (gateway → backends, and S2S)
datastores:
postgres:
tls:
enabled: true # Postgres requires TLS; backends connect sslmode=verify-full
nats:
tls:
enabled: true # NATS requires a client cert (the shared token is retired)
Order of rollout — a fresh install gets all three at once. A brand-new
helm installwith the defaults brings up serviceMtls + Postgres TLS + NATS TLS together cleanly (there's no running datastore to migrate). The staging rule below matters for existing releases, not fresh installs.Upgrading an existing release that ran with all three OFF (staged, not all-at-once). Do not flip all three in a single
helm upgrade. EnableserviceMtls.enabledfirst (one upgrade — it provisions the Platform CA and the per-service certs the Postgres and NATS layers reuse), let the HTTP layer go healthy, then enabledatastores.postgres.tls.enabledanddatastores.nats.tls.enabledin a later upgrade. Each datastore toggle requiresserviceMtls.enabled— they draw their certs and the in-podca.crtfrom it.Toggling
postgres.tlson a live release changes theschema-migrationJob pod template. Theschema-migrationandrls-rolesJob names embed a hash of the rendered pod template, so any pod-template change (TLS toggles, chart version bumps) creates a fresh Job instead of colliding with an immutable existing one — no manual Job deletion needed.
Advanced: chain the mesh off your own root¶
If your security policy forbids any self-signed CA, even an internal one,
point the Platform CA at an intermediate issued from your corporate root
instead of the chart's self-signed bootstrap. Provide a Secret containing
tls.crt + tls.key + ca.crt:
serviceMtls:
enabled: true
platformCa:
externalCaSecretName: vistaplatform-platform-ca # your intermediate CA
The entire mesh then chains to your root. This is a power-user option; the default self-signed Platform CA is appropriate for the large majority of deployments.
Bring-your-own Postgres/NATS (managed services). When
datastores.postgres.enabled: false(e.g. a managed Postgres) the chart can't mint the Postgres server cert. PointserviceMtls.postgres.caSecretName/clientCertSecretNameat your provider's CA and your client cert, and setserviceMtls.postgres.modeto match what the managed service enforces (require/verify-ca/verify-full).
Agent and sensor mTLS (agentMtls) — ON by default, and a breaking upgrade¶
This is a separate toggle from serviceMtls, covering the outbound calls
device agents and sensors make into the platform: job polling, result
submission, heartbeats, discovery submission, command polling and certificate
rotation.
With agentMtls.enabled: false those routes tolerate a missing client
certificate: the agent/sensor UUID in the URL is the entire credential, so any
caller on the pod network that knows (or guesses from a leaked log line) a UUID
can poll that agent's jobs, submit results in its name, or submit discoveries.
It now defaults to true, and the backends fail closed: the certificate
issued at registration must reach the service intact, its CN must be the
agent/sensor id, it must chain to that tenant's CA, and it must be that
identity's current certificate.
Certificate rotation requires the current certificate in every mode, even with
agentMtls.enabled: false. Rotation mints a new, legitimately CA-signed certificate and supersedes the old one, so a path UUID must never be enough to obtain it. An agent or sensor that has lost its certificate cannot rotate — re-enroll it with a registration key.
Prerequisites (cluster infrastructure — the chart does not install these)¶
serviceMtls.enabled: true(the default). The agent listener reuses the per-service mesh certificate as its server certificate.- A cluster-Traefik TLS-passthrough entrypoint on
agentMtls.port(default8444). The edge must not terminate this traffic, or the client certificate never reaches the backend. The chart owns only theIngressRouteTCP, not Traefik's static entrypoint configuration. - A DNS name per backend resolving to that entrypoint, set in values:
agentMtls:
enabled: true
backends:
sensor-manager:
dnsName: sensors.example.com
device-interrogation-service:
dnsName: agents.example.com
Both backends need their own hostname. They share one passthrough
entrypoint and are told apart by HostSNI, so a single name cannot serve
both. Keep them distinct from each other and distinct from
tls.dnsName — that host is terminated at the edge.
The chart fails the install rather than open a listener nothing can
reach. It fails if either hostname is missing, and also if a backend entry
is deleted — removing a key does not opt a service out. The deployment
template keys AGENT_MTLS_REQUIRED off the presence of that entry, so a
missing key emits no value and the service falls back to its built-in
default of true, demanding a client certificate that no route can
deliver. To opt out, set agentMtls.enabled: false.
Dev installs need no DNS entries at all. The docker-compose path sets
AGENT_MTLS_REQUIRED=false for both services explicitly, so a compose
developer never provisions a passthrough host or a certificate.
Migrating an existing install (do this before upgrading)¶
Agents and sensors enrolled while agentMtls was off call the edge host and
hold no usable transport for their certificate. After the upgrade they are
rejected with 401 until migrated.
- Inventory what is enrolled. Discovery → Sensors & Agents lists every agent and sensor with its last check-in.
- Provision the passthrough entrypoint and DNS (prerequisites 2 and 3 above) before the upgrade, and verify the host resolves and accepts TCP on the port.
- Upgrade with the dnsNames set. Every backend restarts.
- Re-point or re-enroll each agent/sensor. Re-registering with a registration key is the reliable path: the registration response hands back the client certificate, the Platform CA as server trust anchor, and the passthrough URL, which the agent persists and uses from then on.
- Confirm. Each agent/sensor returns to a healthy last-check-in; a failure
shows as
401in thesensor-manager/device-interrogation-servicelogs naming the missing or mismatched certificate.
Staging the upgrade in two steps (mesh first, agents second) keeps the blast radius small, exactly as with the datastore toggles above.
Opting out deliberately¶
--set agentMtls.enabled=false
This restores the previous, unauthenticated agent/sensor surface. The chart
prints a warning in NOTES.txt on every install and upgrade while it is off, so
the choice stays visible. Certificate rotation remains refused in this mode.
Single-node clusters: avoid the rolling-upgrade CPU deadlock¶
On a one-node cluster, a helm upgrade that rolls every service at once
briefly runs old + new pods together, doubling CPU requests (the scheduler
packs by requests, not usage). If that exceeds the node's allocatable CPU, the
recreated postgres-0 can't schedule, the schema migration can't connect, and
the upgrade hangs until it times out. Either size the node for ~2× steady-state
CPU requests before upgrading, or set strategy: Recreate on the backends so
pods replace in place without surging (brief per-service downtime; on a
release that is already deployed, see the production checklist's Single-node
clusters for the one-time patch that switch needs). If an upgrade does wedge,
helm rollback restores the previous release.
This applies to config-only upgrades too, not just version bumps: the pod
templates carry a checksum/config hash of the app ConfigMap, so changing any
value that lands there (tls.dnsName, appConfig.*, …) deliberately rolls
every backend — env vars are injected at pod start only, and without the roll
the running pods would silently keep the old config.