.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

  1. Explicit path - starts with / or contains identity.json
  2. Vault ID - looks up ~/.sikkerkey/vaults/{vaultId}/identity.json
  3. SIKKERKEY_IDENTITY env - path to identity file
  4. 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

PropertyTypeDescription
MachineIdstringMachine UUID
MachineNamestringHostname from bootstrap
VaultIdstringVault this identity belongs to
ApiUrlstringSikkerKey API URL

Read-only.

Method Reference

MethodReturnsDescription
SikkerKeyClient.Create(vaultOrPath?)SikkerKeyClientCreate 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)voidWatch a secret for changes
Unwatch(secretId)voidStop watching a secret
SetPollInterval(seconds)voidSet poll interval (min 10s)
Close()voidStop 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

VariableDescription
SIKKERKEY_IDENTITYPath to identity.json - overrides vault lookup
SIKKERKEY_HOMEBase config directory (default: ~/.sikkerkey)

Dependencies

DependencyVersionPurpose
NSec.Cryptography>=25.4.0Ed25519 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.