Skip to main content

Deploying Zaris on Kubernetes With Helm: A Secure End-to-End Walkthrough

· 7 min read
Clustron Team
Distributed Systems Engineering

Deploying Zaris on Kubernetes with Helm, secured end to end

Getting a distributed store onto Kubernetes is easy. Getting it there with TLS on, reachable from outside the cluster, and a client that actually connects is where most walkthroughs quietly stop. Zaris ships a Helm chart, and we've validated the whole path — including the secured, externally-reachable configuration — end to end on a local cluster.

This post is that walkthrough. We run it on kind (Kubernetes-in-Docker) so you can reproduce every step on a laptop, and we take the harder road on purpose: TLS enabled, certificates with the right subject alternative names, external access on, and a client connecting over the secured endpoint. We call this the Model B configuration — externally-reachable and secured, versus an internal-only cluster.

One correctness rule snags almost every first deploy, and it has nothing to do with certificates: the store name in your connection string must equal the deployment's clusterId. We'll flag exactly where that bites. Everything else is identical on a production cluster — kind just gives us a clean, disposable place to prove it.

What "secure" means here​

A Zaris deployment is several nodes plus a manager, each reachable at a stable DNS name. Turning on security means three things line up at once:

  1. TLS is enabled on the node and manager listeners.
  2. Certificates carry the right SANs — every name a client might use to reach a node must appear in that node's certificate, or TLS verification fails.
  3. External access is configured so a client outside the cluster reaches the nodes at a name that is also in the certificate.

The SAN requirement is the subtle one. A verifying client checks the certificate against the name it dialed, so a single-SAN certificate covering only zaris-0 won't validate when a client connects through an external name like zaris.example.com. That's why the secured path uses multi-SAN certificates — one certificate valid for the in-cluster service name, the per-pod name, and the external name.

Zaris provides certificate-generation and sample tooling so this step is reproducible rather than a hand-rolled openssl marathon. You can still do it by hand — the shape is below — but the tooling keeps the SAN set correct every time.

Step 1: A kind cluster​

kind create cluster --name zaris-secure
kubectl cluster-info --context kind-zaris-secure
kubectl create namespace zaris

That's the whole substrate. On a real cluster you'd skip this and point kubectl at your managed control plane; nothing downstream changes.

Step 2: Generate multi-SAN certificates​

The certificates must cover every name a client or peer will use: the in-cluster service name, the per-pod stable name, and an external name we'll expose. The Zaris cert tooling generates a CA and node certificates with exactly this SAN set; here's the equivalent openssl-style shape so you can see what's being asked for:

# 1. A CA the client will trust
openssl req -x509 -newkey rsa:4096 -nodes -days 365 \
-keyout ca.key -out ca.crt \
-subj "/CN=Zaris Dev CA"

# 2. A node certificate whose SANs cover EVERY name used to reach it
openssl req -newkey rsa:2048 -nodes \
-keyout zaris-node.key -out zaris-node.csr \
-subj "/CN=zaris"

cat > san.cnf <<'EOF'
subjectAltName = @alt
[alt]
DNS.1 = zaris-0.zaris.svc # per-pod stable name
DNS.2 = zaris.zaris.svc # in-cluster service name
DNS.3 = zaris.example.com # external name clients dial
EOF

openssl x509 -req -in zaris-node.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extfile san.cnf -days 365 -out zaris-node.crt

Load the CA and the node key/cert pair into the namespace as secrets the chart can mount:

kubectl -n zaris create secret generic zaris-ca \
--from-file=ca.crt=ca.crt

kubectl -n zaris create secret tls zaris-node-tls \
--cert=zaris-node.crt --key=zaris-node.key

The rule to remember: if a name isn't in subjectAltName, a verifying client that dials that name rejects the connection. Get the SAN list right and TLS just works; get it wrong and you'll chase a handshake failure that looks like a network problem but isn't.

Step 3: The values file (TLS + external access)​

Here's a values.yaml sketch for Model B — security on, external access on, and the certificate secrets from step 2 wired in:

# values.yaml — Model B: secured + externally reachable
clusterId: zaris # <-- remember this value for the connection string

replicaCount: 3

tls:
enabled: true
# CA clients must trust, and the node key/cert pair
caSecret: zaris-ca
certSecret: zaris-node-tls

external:
enabled: true
# the name clients outside the cluster will dial —
# must also be a SAN on the node certificate
advertisedHost: zaris.example.com

manager:
tls:
enabled: true
caSecret: zaris-ca
certSecret: zaris-node-tls

Two fields do the heavy lifting. tls.enabled: true turns on the secured listeners and mounts the certificates. external.enabled: true with an advertisedHost makes the nodes reachable from outside — and advertisedHost is the same zaris.example.com we put in the certificate SANs. Those two must agree. Keep an eye on clusterId too: it's zaris here, and you'll need it verbatim in a moment.

Step 4: helm install​

helm install zaris oci://registry-1.docker.io/clustron/zaris \
--version 2.0.0 \
-n zaris \
-f values.yaml

# watch the nodes come up
kubectl -n zaris rollout status statefulset/zaris
kubectl -n zaris get pods

When the pods are ready you have a three-node, TLS-enabled Zaris cluster with an external endpoint. On kind, reach it with a port-forward; on a real cluster a LoadBalancer Service or ingress takes over.

kubectl -n zaris port-forward svc/zaris 7000:7000

Step 5: Connect a client — and the clusterId rule​

Now the part that trips up first deploys. A Zaris connection string names the store you're connecting to, and against a Helm deployment that store name must equal the deployment's clusterId. We set clusterId: zaris in values.yaml, so the store name in the connection string is zaris — not my-store, not the release name, not the Service name. The clusterId.

// The store segment MUST match values.yaml clusterId ("zaris").
// zariss:// selects TLS; ?ca= points at the CA we generated.
var connectionString =
"zariss://zaris.example.com:7000/zaris?ca=file:./ca.crt";

await using var client = await ZarisClient.ConnectAsync(connectionString);
await client.PutAsync("greeting", "hello from a secured cluster");
var value = await client.GetAsync("greeting");

If the store name and clusterId disagree, the connect fails — and the error can read like an auth or routing problem rather than a naming one. When a secured Helm deploy refuses to connect and the certificates check out, this is the first thing to verify: connection-string store name == clusterId. It's the single most common first-deploy failure we see.

Note the scheme: zariss:// (with the extra s) selects the secured endpoint, and ?ca=file:./ca.crt tells the client which CA to trust — the one we minted in step 2. The host is the external name, which is why it had to be a SAN.

From kind to production​

Two things differ on a real cluster: external access becomes a LoadBalancer Service or an ingress with real DNS instead of a kubectl port-forward, and certificates come from your own CA or an issuer like cert-manager rather than a dev CA. What does not change is the important part — the SAN rule (every name a client dials must be on the certificate) and the connection-string rule (store name equals clusterId). You're swapping a port-forward for a LoadBalancer and a dev CA for a real one, not rearchitecting the deploy.

Where to go next​

That's a secured Zaris cluster end to end: Helm-installed, TLS on, multi-SAN certificates, external access, and a client connected over the encrypted endpoint. To go deeper on the trust model — Zaris supports multiple CA modes so you don't have to fight your organization's PKI — read Pluggable TLS Trust. For the full chart reference and production networking patterns, see the Kubernetes deployment guide.