Signet.Client 0.3.0

dotnet add package Signet.Client --version 0.3.0
                    
NuGet\Install-Package Signet.Client -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Signet.Client" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Signet.Client" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Signet.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Signet.Client --version 0.3.0
                    
#r "nuget: Signet.Client, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Signet.Client@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Signet.Client&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Signet.Client&version=0.3.0
                    
Install as a Cake Tool

Signet.Client (C#)

C# client for signet, generated from bytepunx/signet-proto and pinned independently of the server's own release cycle (see buf.gen.yaml).

Mirrors the Go client's design: SignetConnect.DialWorkloadAsync/DialAdmin for connection setup, and Restart.WatchBundleAsync/AcquireLockAsync/WaitForRestartAsync for coordinated rolling restarts — expressed idiomatically for .NET (Task-based async, specific exception types, IAsyncDisposable), not a mechanical transliteration of the Go syntax.

Install

dotnet add package Signet.Client

Published to NuGet automatically by publish-csharp.yml whenever release-please tags a csharp-v* release, via Trusted Publishing (OIDC — no long-lived API key stored in this repo).

Regenerating stubs

buf generate

Pulls signet/v1 and admin/v1 from buf.build/bytepunx/signet-proto and regenerates gen/ (gitignored — regenerate locally; CI regenerates fresh too). Edit the module reference in buf.gen.yaml to pin a different schema version.

Building and testing

dotnet build      # builds both SignetClient and SignetClient.Tests (via SignetClient.sln)
dotnet test        # runs the full test suite

Both commands can also be pointed at a specific project, e.g. dotnet test SignetClient.Tests/SignetClient.Tests.csproj. examples/echo is a minimal, environment-variable-configured console app used by the signet-smoke-test Docker/Kubernetes harness to exercise this library against a real signet + SPIRE cluster; it builds as part of the same dotnet build above. Requires the .NET 8 SDK; if dotnet isn't already on PATH, install it with the official script:

curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
chmod +x /tmp/dotnet-install.sh
/tmp/dotnet-install.sh --channel 8.0 --install-dir /tmp/dotnet-sdk
export PATH="/tmp/dotnet-sdk:$PATH"

Tests use xUnit (the modern .NET default — first-class async Task test methods and Assert.ThrowsAsync for exception-type assertions, versus MSTest/NUnit's extra attribute ceremony for async cases). Every test runs against hand-written fakes (SignetClient.Tests/Fakes/FakeLockStream.cs, FakeWatchStream.cs) implementing the same internal ILockStream/IWatchStream seams the real gRPC-backed implementations use — mirroring the Go client's fake-stream pattern in restart_test.go. None of the ~32 tests open a real network connection or require a live signet instance.

Usage

Workload access (SecretsService, SPIFFE mTLS)

await using var conn = await SignetConnect.DialWorkloadAsync(
    "signet.internal:8443", "unix:///run/spire/sockets/agent.sock", "example.org");

var client = conn.SecretsClient;
var resp = await client.GetSecretAsync(new GetSecretRequest
{
    Namespace = "default", Service = "example", Name = "api-key",
});

Operator access (AdminService/GitOpsService, bearer token)

using var conn = SignetConnect.DialAdmin("localhost:8444", token);

var admin = conn.AdminClient;
var status = await admin.StatusAsync(new StatusRequest());

DialAdmin mirrors Go's exact TLS-selection logic: loopback addresses (the documented kubectl port-forward workflow) default to plaintext; every other address is upgraded to TLS automatically using the system trust store, or a caller-supplied CA bundle; forceTls: true requests TLS even for a loopback address.

Coordinated restarts (no process host, no environment injection)

WatchBundleAsync/AcquireLockAsync/WaitForRestartAsync let a service pull its own configuration directly and in-memory, and safely coordinate a fleet-wide serialized restart when it changes — without kickr (a process-host base image that injects the bundle into a child process's environment) and without ever writing secrets to the OS environment, where they'd be readable via /proc/<pid>/environ by anything sharing the pod's PID namespace.

This library deliberately does not:

  • spawn or supervise a child process — it's embedded directly in the process that's configuring itself from the bundle;
  • write the bundle to environment variables, files, or anywhere outside memory;
  • refetch the bundle after the lock is acquired — since there's no replacement process to hand it to, the next process instance (started fresh by Kubernetes after this one exits) fetches it during its own normal startup;
  • call Environment.Exit anywhere — the caller decides when to actually terminate the process.
var client = conn.SecretsClient;

// Fetch once at startup, configure the app in memory.
var bundle = await client.GetServiceBundleAsync(new GetServiceBundleRequest
{
    Namespace = "default", Service = "example",
});

// ... serve traffic ...

// Block until signet reports a change AND this replica holds the restart lock (at most one
// replica restarts at a time, fleet-wide).
var @lock = await Restart.WaitForRestartAsync(
    client, "default", "example",
    ttl: TimeSpan.FromSeconds(30),      // must cover your graceful shutdown
    debounce: TimeSpan.FromSeconds(10)); // absorb rapid successive changes

// Do your own graceful shutdown: drain in-flight requests, close resources.

await @lock.ReleaseAsync(); // release the lock for the next waiting replica
Environment.Exit(0);         // Kubernetes restarts the pod; the new process fetches fresh config

Lock.Lost is a Task<Exception> that completes, at most once, if the lock is lost unexpectedly (stream error, missed heartbeats) before you call ReleaseAsync — treat completion as "another replica may now restart concurrently." It deliberately never completes for a clean, intentional release, so await Task.WhenAny(lock.Lost, ...) is a reliable way to distinguish the two.

Heartbeats are sent at ttl / 4 — not ttl / 2. That interval matters: signet's restart-lock protocol (see docs/restart-lock.md) is explicit that "4 consecutive missed heartbeats exhaust the TTL," and a too-infrequent heartbeat was a real bug previously found in kickr, signet's existing process-host client.

SPIFFE support: a maturity gap worth knowing about

DialWorkloadAsync is implemented against the third-party Spiffe NuGet package (vurhanau/csharp-spiffe) — a C# reimplementation of go-spiffe (the library Go's DialWorkload depends on). Its API maps directly onto go-spiffe's: X509Source fetches and rotates an X.509 SVID from the Workload API, and Authorizers.AuthorizeMemberOf(trustDomain) rejects a server whose presented SPIFFE ID isn't a member of the expected trust domain — the same semantics as Go's tlsconfig.AuthorizeMemberOf.

Unlike go-spiffe, this is not an official spiffe.io project. As of this writing it's a single-maintainer repository at a pre-1.0 version (0.0.3), with modest adoption (order of a dozen GitHub stars). It is actively maintained — commits and dependency bumps within the last month — and its design is a faithful, well-structured port of go-spiffe, but teams adopting DialWorkloadAsync should weigh that maturity gap before relying on it in production, and pin the exact version rather than trusting broad semver ranges. Unlike the Erlang client, which has no workable path to codegen'd protobuf/gRPC stubs at all yet, C#'s gap is narrower: only the SPIFFE Workload API integration is on comparatively thin ice — DialAdmin, WatchBundleAsync, AcquireLockAsync, and WaitForRestartAsync have no SPIFFE dependency whatsoever and work against any Grpc.Net.Client.GrpcChannel, including one a caller constructs by hand (e.g. against a self-managed mTLS setup) without going through DialWorkloadAsync at all.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 66 7/20/2026