Skip to main content

Secure a .NET client with TLS and a token

In this tutorial, you build a console app that connects to a secured Zaris store — one that encrypts its data plane with TLS and requires a signed bearer token on every connection. You get the cluster CA so your client can trust the nodes, obtain a token from an operator, wire both into the client with the real client API, and run a secured Put and Get. You then make the token self-renewing so long-running apps survive rotation.

This is the .NET-application counterpart to Connecting a secured client, which covers the same flow from PowerShell with Connect-ZrStore. If you have not yet driven the basic client operations, start with Your first store and CRUD — this tutorial assumes you know how to read a KvResult.

Prerequisites

  • The .NET SDK.
  • A running TLS + token-secured cluster to connect to. If you do not have one, stand up the secured Docker Compose cluster in Docker Compose (secured), or ask the operator who ran TLS encryption and Authentication and tokens for its endpoints.
  • The store's client endpoints (host:port), for example node-01:7861.
note

A client connecting to a secured store does two things: it validates each node's certificate against the cluster CA, and it presents a token. TLS authenticates the nodes to the client; the token authenticates the client to the nodes. You need both pieces before you write any code.

1. Create a project

Create a console project and add the SDK, which brings in the client used below.

dotnet new console -o zaris-secure-client
cd zaris-secure-client
dotnet add package Clustron.Zaris.SDK

2. Get the cluster CA certificate

Your client must trust the certificates the nodes present. The trust anchor is the cluster CA. An operator exports the public CA once with the admin cmdlet and gives it to you:

Get-ZrClusterCaCert -OutFile cluster-ca.crt

Copy cluster-ca.crt to the machine that runs your app — for example C:\certs\cluster-ca.crt. The client accepts either a path to this file or its PEM contents. This is the same CA the PowerShell client pins with -TlsCaCert, described in Connecting a secured client.

note

When the cluster uses your corporate CA (see CA and trust modes) and the client machine already trusts that root, you still pass the CA explicitly here — the .NET client validates the node certificate against the CA you give it, not the OS trust store.

3. Obtain a token

Tokens are minted by an operator through the Management Service, as covered in Authentication and tokens. For an application, the operator typically creates a service account and hands you the token it returns:

New-ZrServiceAccount -Name orders-svc -Role DataWriter -Scope zaris:store:orders

The token is shown once — capture it. For a one-off subject without a persistent identity, New-ZrToken -Subject app1 -Role DataWriter -Scope zaris:store:orders issues a token the same way. The token must carry a data-capable role (DataReader, DataWriter, or ClusterAdmin) for the store you connect to, or the connection is rejected.

For this tutorial, put the token in an environment variable so it never appears in source:

# bash
export ZARIS_TOKEN="<paste-the-token>"
# PowerShell
$env:ZARIS_TOKEN = "<paste-the-token>"

4. Configure the secured client

Open Program.cs and replace its contents with the following. You register the store from a single connection string that carries everything a secured client needs, then resolve the client from the provider.

  • zariss:// turns on data-plane TLS, and ca=file:… points the client at the cluster CA from step 2.
  • token=env:ZARIS_TOKEN reads the bearer token from the environment variable you set in step 3.
  • The comma-separated hosts are the seed nodes, and the path (/orders) is the store name.
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

// One connection string: TLS on (zariss://), the cluster CA, the seed nodes,
// the store name in the path, and the token read from ZARIS_TOKEN.
var services = new ServiceCollection()
.AddClustronZaris(
"orders",
"zariss://node-01:7861,node-02:7861/orders?token=env:ZARIS_TOKEN&ca=C:\\certs\\cluster-ca.crt")
.BuildServiceProvider();

IZarisClient client = await services
.GetRequiredService<IZarisClientProvider>()
.GetAsync("orders");

GetAsync opens the connection: it performs the TLS handshake against the seed nodes and presents the token at the Hello. If either the certificate or the token is wrong, it throws here rather than later — see Troubleshooting.

note

A single connection string carries the seeds, store name, TLS (zariss://), CA (?ca=), and a static token (?token=), so the standard DI registration secures a remote client end to end — you no longer need to hand-build a ZarisClientBootstrapper for TLS or tokens. For a token that must rotate while the app runs, keep the connection string for the seeds, store, and TLS and set a TokenProvider in code instead — see step 6, which reads the token on every (re)connect.

Because the store is registered in the service provider, the rest of your app can depend on IZarisClientProvider (or the resolved IZarisClient) by interface, exactly as in Your first store and CRUD.

5. Run a secured Put and Get

Use the client exactly as you would an unsecured one — the token and TLS are transparent once connected. Check IsSuccess before reading a value, just as in the CRUD tutorial.

var put = await client.PutAsync("order:1001", "shipped");

if (!put.IsSuccess)
{
Console.WriteLine($"Put failed: {put.Status} {put.Error}");
return;
}

var get = await client.GetAsync<string>("order:1001");

if (get.IsSuccess)
{
Console.WriteLine($"Got: {get.Value}");
}

Run it:

dotnet run

You see:

Got: shipped

The value crossed the network encrypted, and the nodes served it only because your token carried a data-capable role for the orders store.

6. Rotate the token without reconnecting

A token in the connection string (?token=env:…) is resolved once, when the store is registered. For a token that changes while the app runs, set a TokenProvider (a Func<string?>) in code instead: the client reads it on every (re)connect, so an external rotator can replace the token and the client picks it up seamlessly, without you tearing the client down. Keep the connection string for the seeds, store name, and TLS; parse it and build the client from the parsed values plus the provider. ZarisTokenProviders ships ready-made providers:

// Read from a file that a secret manager or sidecar rewrites:
var tokenProvider = ZarisTokenProviders.FromFile("/var/run/secrets/zaris-token");

// Or from an environment variable (re-read on every reconnect, unlike ?token=env:):
var tokenProvider = ZarisTokenProviders.FromEnvironment("ZARIS_TOKEN");

For a self-renewing token, ZarisRenewingTokenProvider logs in once against the Management Service and keeps a fresh token in the background by renewing shortly before expiry. Parse the connection string for the seeds/store/TLS, hand its Token provider to the client options, and dispose the provider to stop renewal:

using Clustron.Zaris.Abstractions;
using Clustron.Zaris.Abstractions.Connection;
using Clustron.Zaris.Client;

using var renewing = await ZarisRenewingTokenProvider.FromLoginAsync(
managerBaseUrl: "http://mgr-01:7801",
username: "orders-svc",
password: Environment.GetEnvironmentVariable("ORDERS_SVC_PASSWORD")!);

// The connection string carries the seeds, store name, and TLS; the renewing
// provider stays in code (only a static ?token= is expressible in the URI).
var cs = ZarisConnectionString.Parse(
"zariss://node-01:7861/orders?ca=file:C:\\certs\\cluster-ca.crt");

var options = new ZarisClientOptions
{
StoreName = cs.StoreName,
Mode = ZarisClientMode.Remote,
TokenProvider = renewing.Token, // Func<string?> that always returns the latest token
Tls = new ZarisTlsClientOptions { Enabled = cs.Tls, CaCertificate = cs.CaCertRef },
};
foreach (var ep in cs.Endpoints)
options.SeedServers.Add(new ZarisNodeInfo { Host = ep.Host, ClientPort = ep.Port });

IZarisClient client = await ZarisClientFactory.CreateAsync(options);

The credentials are used only for the initial login and are not retained; renewal afterwards presents the current token to the manager. This mirrors the PowerShell Connect-ZrStore -Credential/-ManagementUrl sign-in described in Authentication and tokens.

warning

A static token — ZarisTokenProviders.Static("...") or the ZarisClientOptions.Token field — never renews. When it expires the next reconnect is rejected as unauthorized. Use a provider that re-reads a rotating source (file, environment, or the renewing provider) for anything long-running.

7. Troubleshooting

A secured connect fails fast with an explanatory message instead of hanging, so the two common problems are easy to tell apart. Both surface from the connect (GetAsync, or the first operation) as an exception whose message names the cause.

Unauthorized — bad or expired token

If the token is missing, expired, or carries no data-capable role for the store, the node rejects the Hello and the connect fails with a message ending in:

Authentication was rejected — pass -Credential (username/password) or a valid -Token.

The inner exception is a ClusterRejectedException. Reconnecting with the same token would just be rejected again, so the client stops rather than looping. Fix it by supplying a fresh token that carries DataReader, DataWriter, or ClusterAdmin for the store — reissue with New-ZrServiceAccount/New-ZrToken, or switch to the renewing provider from step 6.

TLS handshake failure — missing or wrong CA

If the CA is missing or does not match the CA that signed the nodes' certificates, the handshake fails with a message like:

TLS handshake failed — the node's certificate wasn't trusted. Check the CA points at the cluster CA, or use InsecureSkipVerify to test.

The inner exception is a System.Security.Authentication.AuthenticationException. Fix it by pointing CaCertificate at the correct cluster-ca.crt (re-export it with Get-ZrClusterCaCert). If instead the connect times out, you are likely reaching a plaintext port with TLS on, or a firewall is in the way — check the endpoint, not the trust anchor.

warning

For a quick local test against a self-signed cluster you can set InsecureSkipVerify = true on ZarisTlsClientOptions, which accepts any server certificate without validation. It defeats TLS's protection against man-in-the-middle attacks — never use it against a real cluster.

For the full list of statuses an operation can return once connected, see the Status and exception reference.

What you built

You connected a .NET client to a store that enforces both encryption and authentication: you supplied the cluster CA so the client trusts the nodes, presented a token that authorizes your operations, and made that token self-renewing so a long-running app survives rotation. Because the token and TLS live on the connection, your data code stayed identical to the unsecured client — the same PutAsync/GetAsync and the same KvResult handling.

Next steps