SeliseBlocks.Secrets.OS 4.0.3

dotnet add package SeliseBlocks.Secrets.OS --version 4.0.3
                    
NuGet\Install-Package SeliseBlocks.Secrets.OS -Version 4.0.3
                    
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="SeliseBlocks.Secrets.OS" Version="4.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SeliseBlocks.Secrets.OS" Version="4.0.3" />
                    
Directory.Packages.props
<PackageReference Include="SeliseBlocks.Secrets.OS" />
                    
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 SeliseBlocks.Secrets.OS --version 4.0.3
                    
#r "nuget: SeliseBlocks.Secrets.OS, 4.0.3"
                    
#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 SeliseBlocks.Secrets.OS@4.0.3
                    
#: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=SeliseBlocks.Secrets.OS&version=4.0.3
                    
Install as a Cake Addin
#tool nuget:?package=SeliseBlocks.Secrets.OS&version=4.0.3
                    
Install as a Cake Tool

SeliseBlocks.Secrets.OS

Where the store lives

Metadata and audit live in a SecretStore database on the calling tenant's own Mongo connection — the DbConnectionString recorded in the root registry when the environment was provisioned. A dev environment's secrets are on the dev cluster, a staging environment's on the other cluster, prod and root on main. Tenants sharing a connection share one SecretStore, and the mandatory TenantId filter on every query is what separates them.

Nothing needs migrating to adopt this. An existing tenant's registry record already names the main connection, so it resolves to exactly the database it used before.

There is no fallback to main. A request whose tenant is missing from the registry, disabled, or recorded without a connection fails rather than reading somewhere else — reading main for a tenant placed elsewhere would return an empty list rather than an error, and a write would put a credential on a cluster nobody expects it on.

Values are the exception when Key Vault is in use: see below.

Configuration

Values live in Azure Key Vault, or — in an environment with no vault provisioned yet — in the same SecretStore database as the metadata.

Variable Required Purpose
Secrets__ValueStore no KeyVault or Database. Overrides the inference below.

With nothing set, the store is inferred: KeyVault when KeyVault__KeyVaultUrl is present, Database when it is not. That way an environment with a vault keeps using it and the values already in it stay readable, while an environment without one gets a working store instead of a startup failure.

The two stores keep their bytes in different places, so a value written to one is invisible to the other. Switching an environment that already holds values means re-entering them, not just setting a variable.

Database stores values unencrypted, in a SecretValues collection of their own so a metadata read can never carry one out with it. It keeps the whole ISecretService contract around them — tenant-scoped authorization, rotation, soft delete, audit — and keeps a value out of the configuration document that owns it and out of every configuration API response. What it does not do is protect a value from anyone who can read the SecretStore database. It is the interim option; KeyVault is the intended production store.

Key Vault

Configured through the KeyVault environment section — the same keys Blocks.Genesis reads, so a host already configured for Genesis needs nothing extra.

Variable Required Purpose
KeyVault__KeyVaultUrl yes Vault URL, e.g. https://my-vault.vault.azure.net/
KeyVault__ClientId no Service-principal application id
KeyVault__ClientSecret no Service-principal secret
KeyVault__TenantId no Directory tenant id

Authentication tries DefaultAzureCredential first — managed identity on Azure, az login / Visual Studio / AZURE_* environment variables locally. When that chain has nothing available and all three of ClientId, ClientSecret, and TenantId are set, it falls back to those. Set them on hosts with no CLI login and no managed identity. A partially filled set is ignored.

Bad credentials surface on the first vault call, not at startup: resolution happens in a synchronous singleton constructor, so probing the token there would block container build on a network round trip and take the host down on a transient AAD blip.

One vault serves every tenant the host serves, whichever cluster their metadata is on. Vault keys come from the globally unique secret id, so tenants cannot collide, but the vault rather than the cluster is the blast radius for values. Separating values per placement means a separate host with its own KeyVault__KeyVaultUrl, not a code change. The Database store has no such split — its values sit beside the metadata, so they follow the tenant's placement automatically.

Setup

services.AddBlocksSecrets();

builder.Services.Configure<MvcOptions>(options => options.Filters.Add<SecretExceptionFilter>());

Inject ISecretService wherever you need it.

Types

Type Access list Value readable by Shown in the Blocks OS UI
api yes the access list, the creator, root reveal, copy, edit, rotate
service no any authenticated caller in the tenant metadata only
both no any authenticated caller in the tenant reveal, copy, edit, rotate

both reads identically to service — a valid tenant context and Active status is the whole check for either. The difference is presentational: the UI offers value and edit actions for both and hides them for service. That makes an existing capability visible rather than granting a new one, because the API has always let any authenticated caller in the tenant read a service value.

The type is fixed at creation. There is no operation that rewrites it, because converting one in place would silently move an existing credential between access models — a service secret turned both would become visible to every user in the tenant without anyone re-approving it.

SecretTypes.HasAccessList is the single predicate behind the distinction; four rules depend on it (whether a create stores an access list, whether UpdateAccessAsync applies, whether a value read consults the list, and whether a metadata mutation does).

Create

var secretId = await _secrets.SetAsync(new SetSecretRequest
{
    Name  = "smtp-password",
    Value = plaintext,
    Type  = SecretTypes.Service,
    Tags  = ["mail", "env:prod"],
});
var ids = await _secrets.SetManyAsync(
[
    new SetSecretRequest { Name = "smtp-password", Value = pwd,  Type = SecretTypes.Service },
    new SetSecretRequest { Name = "smtp-host-key", Value = key,  Type = SecretTypes.Service },
]);
// ids["smtp-password"] => "3f2a...c91"

Store the returned id on your own documents. Never the value.

Read

SecretResult? secret = await _secrets.GetAsync(secretId);

SecretListResult page = await _secrets.FindAsync(new SecretFilter
{
    Search     = "smtp",
    Type       = SecretTypes.Service,
    Tags       = ["iam", "os"],   // any of them, not all
    PageNumber = 1,
    PageSize   = 20,
});
string value = await _secrets.GetValueAsync(secretId);

IReadOnlyDictionary<string, string> values = await _secrets.GetValuesAsync([id1, id2]);

Tags

Tags are free-form labels for grouping and lookup. They carry no authorization meaning — an access list is still what decides who may read a value.

They are normalized on write: trimmed, lowercased, de-duplicated and ordered. A tag must start with a letter or digit and may then contain letters, digits, ., _, - and :; the colon is there so a tag can be namespaced as env:prod. At most 20 tags per secret, 50 characters each.

UpdateAsync replaces the whole set. Null leaves the existing tags alone; an empty collection clears them.

await _secrets.UpdateAsync(secretId, new UpdateSecretRequest { Tags = ["mail", "env:staging"] });

Filtering is any-of: a secret matching at least one of the given tags is returned, so selecting a second tag widens the result rather than narrowing it. At most 10 tags per filter.

The catalogue

GetTagsAsync returns the tenant's tag catalogue — key plus display label — for a tag picker or a tag filter:

IReadOnlyList<SecretTagEntry> tags = await _secrets.GetTagsAsync();
// [{ Key = "iam", Label = "Blocks Iam" }, …]

It lives in the tenant database's keyValueStores collection under my-secret-tags, read and written through Genesis's IKeyValueStore:

{
  "Key": "my-secret-tags",
  "Value": [
    { "Key": "iam", "Label": "Blocks Iam" },
    { "Key": "os",  "Label": "Blocks Logic" }
  ]
}

Entries seeded by hand keep the labels they were given. When a create or update carries a tag the catalogue has not seen, the platform adds it with a label derived from the key (payments-team becomes "Payments Team"); an existing key is never rewritten. Nothing removes an entry, so a tag stays on offer after the last secret using it is gone.

The catalogue is a convenience list, not an authority. It never constrains what a secret may be tagged with and is never consulted to validate a tag, so a missing document means "no suggestions" rather than an error, and a failed catalogue write never fails the secret write that triggered it.

Update and rotate

await _secrets.UpdateAsync(secretId, new UpdateSecretRequest
{
    Name        = "smtp-password-primary",
    Description = "Outbound mail",
});

await _secrets.RotateAsync(secretId, new RotateSecretRequest { Value = newPlaintext });

Lifecycle

await _secrets.LockAsync(secretId);
await _secrets.UnlockAsync(secretId);
await _secrets.DeleteAsync(secretId);
await _secrets.RestoreAsync(secretId);

Access

await _secrets.UpdateAccessAsync(secretId, new SecretAccess
{
    UserIds = { "user-1", "user-2" },
    Roles   = { "admin" },
});

Audit

SecretAuditListResult logs = await _secrets.GetAuditLogsAsync(new SecretAuditFilter
{
    SecretId   = secretId,
    FromDate   = DateTime.UtcNow.AddDays(-30),
    PageNumber = 1,
    PageSize   = 20,
});

Errors

Exception HTTP
SecretValidationException 400
SecretAccessDeniedException 403
SecretNotFoundException 404
SecretStateException 409
SecretVaultException 502

A tenant that is unknown to the registry or disabled raises SecretAccessDeniedException (INVALID_CONTEXT), the same as an unauthenticated caller — the store it would have been routed to is not confirmed either way. A tenant recorded without a connection is a provisioning fault, not a caller error, and surfaces as an unmapped failure the host reports as 500.

HTTP

Route Method Purpose
/Secrets/gets GET List metadata; ?tags=iam&tags=os filters to secrets carrying any of them
/Secrets/tags GET The tenant's tag catalogue, for a picker or a filter
/Secrets/values POST Batch plaintext read by id
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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 (1)

Showing the top 1 NuGet packages that depend on SeliseBlocks.Secrets.OS:

Package Downloads
SeliseBlocks.ReleaseDriver.OS

Blocks Release Driver

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.3 2,677 9/22/2026
4.0.2 496 9/8/2026
4.0.1 1,050 8/23/2026
4.0.0 184 8/19/2026

The secret store now follows the tenant's database placement: metadata, audit and database-backed values resolve on the connection recorded in the tenant registry instead of always on the main connection. Existing tenants are unaffected, their registry record already names main. An unknown, disabled or unprovisioned tenant now fails instead of reading main. Index creation is ensured once per cluster rather than once per process. SecretStoreContext takes ITenants in place of IBlocksSecret; hosts that resolve it from the container need no change. Key Vault values still live in the one vault the host is configured with, whichever cluster a tenant is on.