.NET SDK
Read secrets from any C#/.NET application using Ed25519 machine authentication.
Installation
dotnet add package SikkerKey
Requires .NET 8.0+. Single dependency: NSec.Cryptography for Ed25519 signing.
Quick Start
using SikkerKey;
var sk = SikkerKeyClient.Create("vault_abc123");
var secret = await sk.GetSecretAsync("sk_a1b2c3d4e5");
The SDK loads the machine identity from ~/.sikkerkey/vaults/{vaultId}/identity.json, signs every request with the machine's Ed25519 private key, and returns the decrypted value. All data methods are async.
Client Creation
// Explicit vault ID
var sk = SikkerKeyClient.Create("vault_abc123");
// Direct path to identity file
var sk = SikkerKeyClient.Create("/etc/sikkerkey/vaults/vault_abc123/identity.json");
// Auto-detect from SIKKERKEY_IDENTITY env or single vault on disk
var sk = SikkerKeyClient.Create();
Throws ConfigurationException if the identity is missing, the key can't be loaded, or multiple vaults exist without a specified vault ID.
Reading Secrets
Single Value
var apiKey = await sk.GetSecretAsync("sk_stripe_prod");
Structured (Multiple Fields)
var fields = await sk.GetFieldsAsync("sk_db_prod");
var host = fields["host"]; // "db.example.com"
var password = fields["password"]; // "hunter2"
Throws SecretStructureException if the secret value is not a JSON object.
Single Field
var password = await sk.GetFieldAsync("sk_db_prod", "password");
Throws FieldNotFoundException if the field doesn't exist. The error message includes available field names.
Listing Secrets
// All secrets this machine can access
var secrets = await sk.ListSecretsAsync();
foreach (var s in secrets)
Console.WriteLine($"{s.Id}: {s.Name}");
// Secrets in a specific project
var projectSecrets = await sk.ListSecretsByProjectAsync("proj_abc123");
Returns List<SecretListItem> with Id, Name, FieldNames (nullable), and ProjectId (nullable).
Export
// All secrets as a flat dictionary
var env = await sk.ExportAsync();
// Scoped to a project
var env = await sk.ExportAsync("proj_production");
// Inject into environment
foreach (var (key, value) in await sk.ExportAsync())
Environment.SetEnvironmentVariable(key, value);
Structured secrets are flattened: SECRET_NAME_FIELD_NAME.
Watching for Changes
Watch secrets for real-time updates. When a secret is rotated, updated, or deleted, the callback fires with the new value.
sk.Watch("sk_db_credentials", (e) =>
{
switch (e.Status)
{
case WatchStatus.Changed:
// e.Value has the new value
// e.Fields has parsed key-value pairs for structured secrets
Database.ConfigureCredentials(e.Fields!["username"], e.Fields["password"]);
break;
case WatchStatus.Deleted:
Console.WriteLine("Secret deleted");
break;
case WatchStatus.AccessDenied:
Console.WriteLine("Access revoked");
break;
case WatchStatus.Error:
Console.WriteLine($"Error: {e.Error}");
break;
}
});
Polling starts automatically on the first Watch() call and runs on a background task. Default interval is 15 seconds (server enforces a 10-second minimum).
sk.SetPollInterval(30); // seconds
sk.Unwatch("sk_db_credentials"); // stop watching one secret
sk.Close(); // stop all watches
SikkerKeyClient implements IDisposable for automatic cleanup with using blocks.
Multi-Vault
var prod = SikkerKeyClient.Create("vault_a1b2c3");
var staging = SikkerKeyClient.Create("vault_x9y8z7");
var prodKey = await prod.GetSecretAsync("sk_api_key");
var stagingKey = await staging.GetSecretAsync("sk_api_key");
List Registered Vaults
var vaults = SikkerKeyClient.ListVaults();
// ["vault_a1b2c3", "vault_x9y8z7"]
Static method, synchronous.
Identity Resolution
- Explicit path - starts with
/or containsidentity.json - Vault ID - looks up
~/.sikkerkey/vaults/{vaultId}/identity.json SIKKERKEY_IDENTITYenv - path to identity file- Auto-detect - single vault on disk
The vault_ prefix is added automatically if not present. Override base directory with SIKKERKEY_HOME.
Error Handling
using SikkerKey;
try
{
var secret = await sk.GetSecretAsync("sk_nonexistent");
}
catch (NotFoundException)
{
// 404 - secret doesn't exist
}
catch (AccessDeniedException)
{
// 403 - machine not approved or no grant
}
catch (AuthenticationException)
{
// 401 - invalid signature or unknown machine
}
catch (ApiException e)
{
// Any other HTTP error
Console.WriteLine(e.HttpStatus);
}
Exception Hierarchy
SikkerKeyException
├── ConfigurationException - identity/key issues
├── SecretStructureException - secret is not a JSON object (GetFieldsAsync)
├── FieldNotFoundException - field not in structured secret (GetFieldAsync)
└── ApiException - HTTP error (has HttpStatus property)
├── AuthenticationException - 401
├── AccessDeniedException - 403
├── NotFoundException - 404
├── ConflictException - 409
├── RateLimitedException - 429
└── ServerSealedException - 503
Properties
| Property | Type | Description |
|---|---|---|
MachineId | string | Machine UUID |
MachineName | string | Hostname from bootstrap |
VaultId | string | Vault this identity belongs to |
ApiUrl | string | SikkerKey API URL |
Read-only.
Method Reference
| Method | Returns | Description |
|---|---|---|
SikkerKeyClient.Create(vaultOrPath?) | SikkerKeyClient | Create client (static, sync) |
SikkerKeyClient.ListVaults() | List<string> | List registered vault IDs (static, sync) |
GetSecretAsync(secretId) | Task<string> | Read a secret value |
GetFieldsAsync(secretId) | Task<Dictionary<string, string>> | Read structured secret |
GetFieldAsync(secretId, field) | Task<string> | Read single field |
ListSecretsAsync() | Task<List<SecretListItem>> | List all accessible secrets |
ListSecretsByProjectAsync(projectId) | Task<List<SecretListItem>> | List secrets in a project |
ExportAsync(projectId?) | Task<Dictionary<string, string>> | Export as env map |
Watch(secretId, callback) | void | Watch a secret for changes |
Unwatch(secretId) | void | Stop watching a secret |
SetPollInterval(seconds) | void | Set poll interval (min 10s) |
Close() | void | Stop all watches, shut down polling |
Retry Behavior
429 and 503 responses are retried up to 3 times with exponential backoff (1s, 2s, 4s). Each retry uses a fresh timestamp and nonce. Network errors (HttpRequestException, TaskCanceledException) are also retried.
Environment Variables
| Variable | Description |
|---|---|
SIKKERKEY_IDENTITY | Path to identity.json - overrides vault lookup |
SIKKERKEY_HOME | Base config directory (default: ~/.sikkerkey) |
Dependencies
| Dependency | Version | Purpose |
|---|---|---|
NSec.Cryptography | >=25.4.0 | Ed25519 key loading and signing |
All other functionality uses .NET built-ins: System.Net.Http, System.Text.Json, System.Security.Cryptography.
Types
public record SecretListItem(string Id, string Name, string? FieldNames, string? ProjectId);
All HTTP requests have a 15-second timeout. HTTPS enforced for non-localhost.