Am.Keyward.Api
0.14.3-preview
dotnet add package Am.Keyward.Api --version 0.14.3-preview
NuGet\Install-Package Am.Keyward.Api -Version 0.14.3-preview
<PackageReference Include="Am.Keyward.Api" Version="0.14.3-preview" />
<PackageVersion Include="Am.Keyward.Api" Version="0.14.3-preview" />
<PackageReference Include="Am.Keyward.Api" />
paket add Am.Keyward.Api --version 0.14.3-preview
#r "nuget: Am.Keyward.Api, 0.14.3-preview"
#:package Am.Keyward.Api@0.14.3-preview
#addin nuget:?package=Am.Keyward.Api&version=0.14.3-preview&prerelease
#tool nuget:?package=Am.Keyward.Api&version=0.14.3-preview&prerelease
AM KEYWARD
⚠️ Pre-1.0 — no external security review yet. AM KEYWARD is usable and the design is documented in full below, but no independent party has audited the crypto, the tenant isolation or the audit chain. You can run it with real secrets; do so knowing that assurance rests on this project's own testing, and weigh that against what those secrets protect. See SECURITY.md.
AM KEYWARD is an open-source, .NET-native, library-first credential & secrets manager: a building block you embed in your own .NET environment (and can offer to your own users), plus an optional standalone reference app.
It covers two halves equally:
- Software credentials — machine/integration secrets (API keys, connection strings), scoped per project & environment, fetched by your software via an API. Each value can carry a rotation date and a note on how a new one is obtained; the date is advisory (reads keep working) and raises advance notices.
- Human vaults — personal & team password vaults, shared to groups or individuals.
What it is / is not
- Is: an embeddable toolkit + reference app that you self-host and operate.
- Is not: a hosted service, and not a HashiCorp-Vault-at-scale replacement.
There is no central, vendor-hosted AM KEYWARD. Each operator runs and secures their own deployment (key custody, database, hardening) — see SECURITY.md.
How the human-vault half differs from Bitwarden / KeePass
AM KEYWARD is not trying to out-feature a dedicated password manager. Its human vaults exist so that one system covers both halves of an organization's secrets — the machine credentials your software reads at runtime and the passwords your people share — under one data model, one login, one admin, one tamper-evident audit log, with the same envelope encryption and the same operator-owned KEK.
- vs KeePass (a local encrypted file): AM KEYWARD is server-side and multi-user with central authorization grants, tenancy isolation, versioning and audit — not a file you sync by hand.
- vs Bitwarden (an excellent hosted/self-hosted password manager): AM KEYWARD is embeddable in your own .NET app and unifies software credentials with human vaults; it is not a hosted service and does not (yet) ship browser-extension autofill or zero-knowledge vaults (zero-knowledge is the v0.2 goal, gated on an external review). If you only need a password manager for people, use Bitwarden.
Threat model (summary)
What AM KEYWARD is designed to resist, and what it explicitly does not:
In scope / mitigated
- Database compromise alone — a stolen DB backup yields only ciphertext; the KEK lives outside the database, so without the KEK store the envelopes cannot be decrypted.
- Cross-tenant / cross-user leakage — defense-in-depth: a composite application query filter, a
server-authoritative active tenant, and SQL Server row-level security via
SESSION_CONTEXT. The RLS layer holds whatever login the runtime uses — a security policy's filter predicates apply to every principal,db_ownerincluded, unless the predicate exempts them, and ours do not. A least-privilege runtime login is therefore optional hardening (it withholds the right to disable the policy), not what makes the isolation work. Exercised by an adversarial isolation test gate. - Ciphertext replay across slots — AEAD AAD binds every ciphertext to its exact tenant / owner / project / environment / item / version, so a Dev/old ciphertext cannot be moved into a Prod/current row.
- Audit tampering — a per-tenant hash chain, single-writer/serializable append, with an out-of-band chain-head checkpoint so a DB admin who rewrites history is detectable.
- Insider / admin abuse of emergency access — server-side recovery is dual-control break-glass with an out-of-band, append-only non-repudiation trail.
- Secrets in telemetry — the encrypted envelope is redacted from logs; problem-details and provider exceptions never echo values or connection strings.
- Token abuse — software-client tokens are env-scoped (scope from the persisted token, never the request), hashed at rest, rotatable/revocable, rate-limited, with advance-expiry notifications.
Out of scope / operator-owned
- KEK custody and loss — losing the KEK is total, unrecoverable data loss; the operator backs it up (offline, split) and rehearses the restore. See the operations runbook.
- The escrow read path returns plaintext by design — for server-side vaults and the software API, the server can decrypt; this is the acknowledged trade-off of escrow (zero-knowledge human vaults are v0.2).
- Host hardening — OS, network exposure, TLS, and the security of the identity provider are the operator's responsibility.
- No external security review yet (pre-1.0) — nothing here has been audited by an independent party, so the assurance behind every mitigation above is this project's own testing. That is a real limit on how much weight the list deserves, not a reason it cannot be used; judge it against the value of what you store.
Security & operations
- SECURITY.md — reporting, operator responsibilities, no-warranty.
- Operations & KEK/DR runbook — key custody, backup/restore order, KEK rotation and compromise response, monitoring/health endpoints, break-glass, GDPR erasure.
Embedding in your own ASP.NET Core / Blazor app
AM KEYWARD is library-first: you add it to your own .NET 10 Blazor Web App (interactive server) and it
brings its services, API, and feature UI. Reference it either as the published preview NuGet packages
(dotnet add package Am.Keyward.Infrastructure --prerelease, same for Am.Keyward.AspNetCore,
Am.Keyward.Ui.Blazor, and Am.Keyward.Api if you expose the REST APIs) or as direct ProjectReferences
(e.g. a git submodule). Deployed applications that only read their secrets reference the lightweight
Am.Keyward.Client package instead — see
Consuming secrets from a deployed application.
0. Prerequisites — the Keyward pages are interactive-server and [Authorize]-protected, so your app
needs interactivity, auth-state cascading and (that's all — localization comes with AddKeywardUi):
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState(); // [Authorize]/<AuthorizeView> need the cascade
// ...plus your own cookie/OIDC authentication setup.
1. Register the services — one call wires up the EF Core DbContext (schema amkeyward), envelope
crypto, audit, vaults, tokens, break-glass and monitoring:
// The KEK comes from your provider (Azure Key Vault / HSM / a protected file) — NEVER from the DB/appsettings.
// On Windows, DpapiKekFile is the packaged recommended custody: a DPAPI-protected file in a directory YOU
// choose (outside the database and outside any folder a deploy replaces), created on first run:
// var (kek, kekId, created) = DpapiKekFile.LoadOrCreate(@"C:\ProgramData\<Company>\<App>\keyward");
// `created` is your cue to log the "back it up offline" warning once — see the escrow note below.
var keywardConn = builder.Configuration.GetConnectionString("Keyward")!;
var (kek, kekId) = LoadKekFromYourProvider();
builder.Services.AddKeyward(keywardConn, kek, kekId);
// Prefer this overload when the KEK stays in a KMS/HSM (the raw key never enters the process). Supply your
// own IKekProvider, or a KeyRingKekProvider holding the current + prior versions during a KEK rotation:
// builder.Services.AddKeyward(keywardConn, sp => new KeyRingKekProvider(currentKekId, keksByVersion));
// Tell the embedded UI which tenant to operate in. A SINGLE-ORGANIZATION host takes the packaged glue —
// one call registers the fixed-tenant IKeywardWorkspaceContext AND the circuit handler that pins the tenant
// scope (pair it with app.UseKeywardSingleTenant below for the HTTP/SSR path). A MULTI-TENANT host instead
// implements IKeywardWorkspaceContext (Am.Keyward.Core.Abstractions) from its own selection and calls
// ITenantScopeSetter.SetTenant itself.
builder.Services.AddKeywardSingleTenant(myTenantId);
// Register the UI's own services (circuit-scoped state + localization for the Keyward strings, six
// languages built in).
// ProductName is what your users see (browser tab, brand, texts, e-mails); default "AM KEYWARD".
// PublicBaseUrl (optional) enables absolute links in notification e-mails sent by background jobs.
// NotificationLanguage (optional) sets the language for those background e-mails (account e-mails
// follow the request culture); default English.
// TokenEnvironmentVariableName / ClientApiBasePath (optional) only shape the ready-to-run PowerShell
// snippet shown next to a freshly issued app token. By default the variable name is derived PER
// APPLICATION (application "Bvd.Li.Toolbox" -> KEYWARD_BVD_LI_TOOLBOX_TOKEN), so two applications on one
// host never collide; set TokenEnvironmentVariableName only if every application here reads one fixed
// variable. ClientApiBasePath is the path you mapped MapKeywardClientApi at.
builder.Services.AddKeywardUi(o =>
{
o.ProductName = "Contoso Secrets";
o.PublicBaseUrl = "https://secrets.contoso.com";
o.NotificationLanguage = "en";
o.TokenEnvironmentVariableName = "CONTOSO_SECRETS_TOKEN"; // default: derived per application
o.ClientApiBasePath = "/keyward/api/v1"; // default
});
// Optional — transient notifications ("Vault created", "Moved", errors). Keyward depends only on the
// IKeywardNotifier port. Standalone it shows its own BlazorBlueprint-styled toast (KeywardToastHost). If
// your app already has a toast system, override the port so Keyward's notifications use it and are
// indistinguishable from the rest of your app (registered with TryAdd, so your registration wins):
builder.Services.AddScoped<IKeywardNotifier, MyToastNotifier>(); // e.g. routes onto BlazorBlueprint's BbToast
2. Bind identity at the edge. The libraries are identity-agnostic: they read ICurrentUser /
ICurrentTenant, which you set from your auth. Your auth layer stamps the Keyward AppUser id onto the
signed-in principal as the KeywardClaims.UserId claim (however you map it — ASP.NET Identity, external
OIDC, ...); the Am.Keyward.AspNetCore package then establishes the current user on both the HTTP and
the Blazor-circuit path for you:
builder.Services.AddKeywardBlazorUserScope(); // circuit handler: current user per Blazor circuit
// ...
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery(); // MUST come after auth — tokens bind to the signed-in user
app.UseKeywardCurrentUser(); // middleware: current user per HTTP request
app.UseKeywardSingleTenant(myTenantId); // single-organization host: pin the tenant on the HTTP path
Creating those Keyward records is done for you. IKeywardIdentityBinder (registered by AddKeyward)
finds or just-in-time creates the AppUser for your user id, keeps its flags in sync and reconciles the
tenant membership — including removing it when you withdraw vault access. All you decide is what the user
may be, from your own access model. In ASP.NET Core that is a claims factory plus one call:
public sealed class MyKeywardClaimsFactory(
UserManager<MyUser> users, RoleManager<IdentityRole> roles, IOptions<IdentityOptions> options,
IKeywardIdentityBinder binder, ILogger<MyKeywardClaimsFactory> logger)
: UserClaimsPrincipalFactory<MyUser, IdentityRole>(users, roles, options)
{
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(MyUser user)
{
var identity = await base.GenerateClaimsAsync(user); // your role claims
var isAdmin = identity.HasClaim(Options.ClaimsIdentity.RoleClaimType, "Admin");
await KeywardClaimsBinding.ApplyAsync(
identity, binder, user.Id, user.UserName ?? user.Id, myTenantId,
isAdmin ? KeywardIdentityBinding.Administrator : KeywardIdentityBinding.Member,
logger);
return identity; // Keyward unavailable? The user still signs in, only Keyward pages are dead.
}
}
KeywardClaims.UserIdmust be a GUID that exists in Keyward'sUserstable (AppUser). UseIKeywardIdentityBinder(above) — it creates theAppUserand theTenantMembershipfor you. TheTenantitself is seeded once at startup:KeywardSingleTenantSeeder.EnsureSeededAsync(...)covers the fixed tenant and, optionally, your own machine-secrets application. Without these rows the pages render empty and read-only.- Tenant selection stays yours where it is app-specific: a multi-tenant host registers a
CircuitHandlerthat callsITenantScopeSetter.SetTenant(...)inOnCircuitOpenedAsyncwith the same tenant itsIKeywardWorkspaceContextreturns, and pins the same tenant per HTTP request. A single-organization host gets both fromAddKeywardSingleTenant+UseKeywardSingleTenant. If either scope is missing, pages fail with "Tenant scope mismatch" — the HTTP one bites first, on a prerendering page. - Symptom check: a Keyward page stuck at "Loading…" for a signed-in user means the circuit has no
Keyward user — verify
AddKeywardBlazorUserScope()is registered and the principal carries a valid-GUIDKeywardClaims.UserId.
3. Discover the feature pages. The pages live under the /amkeyward/* route prefix (so they can't
collide with your routes). Add the RCL assembly to the endpoint and the router — and route through
AuthorizeRouteView (a plain RouteView ignores [Authorize]):
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(Am.Keyward.Ui.Blazor.KeywardRoutes).Assembly);
<Router AppAssembly="..." AdditionalAssemblies="new[] { typeof(Am.Keyward.Ui.Blazor.KeywardRoutes).Assembly }">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>@* redirect to your login *@</NotAuthorized>
</AuthorizeRouteView>
</Found>
</Router>
4. Drop the navigation into your layout (localized, auth-aware, no hardcoded routes):
<KeywardNav />
Style its .nav-link / .nav-group-label classes in your layout (see the reference shell's
NavMenu.razor.css) — the nav deliberately adopts the host's look. KeywardNav lists the end-user pages;
place links to the admin pages (KeywardRoutes.Groups, KeywardRoutes.DefaultEnvironments) in your own
administration section.
5. Optional REST APIs. The software-client read API (deployed apps fetch their secrets with a bearer token) needs the service registration, the rate-limiter middleware AND the endpoint mapping — the endpoints don't exist otherwise:
builder.Services.AddKeywardSoftwareClientApi(); // "Keyward.SoftwareClient" scheme + rate-limiter policy
// ...
app.UseRateLimiter(); // the mapped group requires the middleware
app.MapKeywardClientApi(); // GET /keyward/api/v1/secrets[/{key}]
// Optional management REST API, guarded by YOUR admin policy:
app.MapKeywardApi(authorizationPolicy: "YourAdminPolicy");
With the client API mapped, Keyward also records access statistics per app token (last access + IP,
requests per day, seen IPs) and derives rule-based access alerts (never-seen IP, active again after a
long silence) — visible on each application's «Statistics» tab and optionally e-mailed to opted-in admins.
Recording is in-memory on the hot path and persisted batched; tune or disable via the Keyward:TokenAccess
section (Enabled, FlushIntervalSeconds, RetentionDays, SilenceAlertDays). Details:
docs/software-client-api.md. Behind a proxy, configure forwarded-headers
middleware so the recorded IP is the client's, not the proxy's.
Building on that signal, heartbeat monitoring (a dead-man's switch per app token) alarms on the
failure an application cannot report itself: a scheduled task that never starts sends no error mail — it
just goes silent. Enable a monitor on the application's «Monitoring» tab (maximum silence, weekday/time
watch window so a Mo–Fr job doesn't false-alarm over the weekend, optional all-clear mail, snooze);
consumers that load their configuration through Keyward need no change at all, long-running services ping
GET /ping (KeywardSecretsClient.PingAsync). Configure via Keyward:Monitoring (Enabled,
CheckIntervalSeconds, TimeZone). TimeZone is the installation's zone — it governs the watch windows,
the statistics day buckets and the timestamps in notification mails; unset it defaults to the server's
local zone. Timestamps are stored in UTC and shown in the viewer's own local time zone in the UI.
Consuming secrets from a deployed application (Am.Keyward.Client)
The consumer side of the software-client API is one line in the deployed application's Program.cs — no
hand-rolled HTTP. Am.Keyward.Client is deliberately lightweight (no EF Core, no server code; just the
configuration provider and a typed HTTP client):
builder.Configuration.AddKeywardSecrets(o =>
{
o.ServiceUri = new Uri("https://keyward.example.com");
o.ApplicationName = "Bvd.Li.Toolbox"; // token read from KEYWARD_BVD_LI_TOOLBOX_TOKEN
});
The bulk read (GET /secrets) lands in IConfiguration, so ConnectionStrings:Main, options binding
etc. resolve like any other configuration value — later sources overlay earlier ones as usual. The token
comes from the same per-application environment variable the Keyward UI's deployment PowerShell snippet
sets on the host (set o.TokenEnvironmentVariableName for a fixed name, or o.Token to pass it directly).
Defaults are deliberate: the source fails the host loudly at startup when no token is deployed or the
server is unreachable (an app whose connection strings live in Keyward must not silently start without
them; set o.Optional = true to tolerate it), and the startup load retries with backoff
(LoadRetryCount/LoadRetryDelay) so a service booting right after a host reboot does not lose the race
against the Keyward server. Set o.ReloadInterval to re-read periodically and raise a configuration reload
when values changed (IOptionsMonitor<T> picks it up); a failed refresh keeps the last known good values.
For direct runtime reads (a secret fetched on demand rather than at startup), register the typed client via
IHttpClientFactory and inject KeywardSecretsClient:
builder.Services.AddKeywardSecretsClient(o =>
{
o.ServiceUri = new Uri("https://keyward.example.com");
o.ApplicationName = "Bvd.Li.Toolbox";
});
// ...then: string? value = await keywardSecretsClient.GetAsync("ConnectionStrings:Main", ct);
Styling and routes come for free. The UI theme is component-scoped CSS in the RCL, so it is folded into
your app's standard {Assembly}.styles.css bundle automatically — no extra stylesheet <link> needed (the
template's existing bundle link covers it). Override the look by redefining the --kw-* CSS variables on
.keyward-ui; dark mode / color themes react to html.dark / html[data-theme="…"] if your host toggles
them (see the reference shell's js/keyward-theme.js) — with no toggling you get the light default. The
vault/application pages use <KeywardUi Fill="true">: in a plain layout they render with a normal page
scroll; for the pinned header + independently scrolling panes, make your content container a fixed-height
flex column and add .your-content:has(> .keyward-ui.kw-fill) { overflow: hidden; display: flex; flex-direction: column; } (see the reference shell's MainLayout.razor.css). To pin the UI language, add
UseRequestLocalization with your supported cultures; without it the pages follow the server culture
(en/de/fr/it/es/pt ship built in). The feature routes are the /amkeyward/* namespace; use the
KeywardRoutes constants for links.
Database & migrations
- You provide the database via the connection string passed to
AddKeyward(connectionString, …). AM KEYWARD is SQL Server only (incl. Azure SQL). The reference shell reads it fromConnectionStrings:Keyward(override it inappsettings.json, an environment variable, or user-secrets; it falls back to alocalhostdev default). AM KEYWARD never hardcodes the database name — that lives in your connection string. - It coexists in your database. All AM KEYWARD tables live in a dedicated schema
amkeywardwith a schema-scoped migrations-history table, so you can point it at the same database your host app already uses without colliding with your tables or your own EF migrations. - Migrations are not automatic just from the connection string.
AddKeywardonly registers theDbContext; the connection string only says where the database is. Apply Keyward's migrations one of these ways (the reference shell does the first two):- call
MigrateAsync()at startup — this creates the database if it doesn't exist and applies pending migrations (using Am.Keyward.Infrastructure.Persistence;):await using var scope = app.Services.CreateAsyncScope(); await scope.ServiceProvider.GetRequiredService<KeywardDbContext>().Database.MigrateAsync(); - and/or run a periodic safety-net (re-applies pending migrations if the DB is swapped under a running
app) — see the reference shell's
DatabaseMigrationBackgroundServiceand theDatabaseMigrationconfig section (Enabled,CheckIntervalSeconds); - or apply them out-of-band in your deploy step (
dotnet ef database update).
- call
- Two database logins — optional hardening, not a prerequisite. Tenant row-level security is enforced
against every principal,
db_ownerincluded, so a single privileged connection gets the same isolation; the split only withholds the right to disable the policy or alter the schema. Adopt it where it is cheap (a dedicated AM KEYWARD database), skip it where AM KEYWARD is embedded in a host's own database and a password would have to be created, deployed and rotated for that one property. If you adopt it, apply migrations out-of-band with the migrator login (the runtime login cannot DDL) and giveAddKeywardonly the runtime connection string; rundb/setup-logins.sqlonce after the first migration. See docs/database-logins.md.
UI design principle — match BlazorBlueprint
The embedded Keyward UI MUST follow BlazorBlueprint's behaviour and layout — its controls, spacing,
interaction patterns and notifications should be indistinguishable from a BlazorBlueprint host, so Keyward
looks and feels native inside one. Keyward stays self-contained (no hard dependency on BlazorBlueprint,
its own --kw-* tokens and components) so it also embeds anywhere; this principle governs that its own UI is
kept visually and behaviourally aligned with BlazorBlueprint's conventions.
Two layers realise this:
- Behaviour via ports. Where a host has a better-integrated primitive, Keyward depends on a small port,
not on the concrete control, and the host wires the real thing. The transient-notification port
IKeywardNotifieris the reference: a BlazorBlueprint host overrides it to useBbToast(real BB, identical to the rest of the app); standalone, Keyward's built-inKeywardToastHostrenders a BlazorBlueprint-styled toast (bottom-right, auto-dismiss, per-kind accent) that mimics it. - Layout via the token contract. Keyward's own components style against
--kw-*tokens chosen to match BlazorBlueprint's look; a host maps them to its theme.
Consequence for contributions: new Keyward UI, and any review of existing UI, is measured against BlazorBlueprint — if a control or message deviates from how BlazorBlueprint would present it, adjust Keyward (or add a port the host fills), don't diverge. The current UI is being reviewed against this and adjusted where it deviates (the notification split — success → toast, errors → inline — was the first pass).
Tech
.NET 10 · Blazor Server · ASP.NET Core · EF Core (Microsoft SQL Server) · MIT licensed.
Build
dotnet build Am.Keyward.slnx
dotnet test Am.Keyward.slnx
Requires the .NET 10 SDK; data/integration tests require a local SQL Server reachable on localhost
via Integrated Security.
Documentation
End-user & operator documentation lives in docs/ and grows as features ship:
- Software-client API — how a deployed app fetches its secrets with a token.
- Database logins — the least-privilege runtime login vs. the migration login that underpins tenant isolation.
License
MIT © 2026 Ardimedia Anstalt
| Product | Versions 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. |
-
net10.0
- Am.Keyward.Contracts (>= 0.14.3-preview)
- Am.Keyward.Core (>= 0.14.3-preview)
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.14.3-preview | 73 | 9/4/2026 |
| 0.14.2-preview | 110 | 8/25/2026 |
| 0.14.1-preview | 158 | 8/17/2026 |
| 0.14.0-preview | 78 | 8/16/2026 |
| 0.13.2-preview | 103 | 8/13/2026 |
| 0.13.1-preview | 66 | 8/13/2026 |
| 0.13.0-preview | 65 | 8/13/2026 |
| 0.12.0-preview | 68 | 8/13/2026 |
| 0.11.4-preview | 103 | 8/8/2026 |
| 0.11.3-preview | 66 | 8/8/2026 |
| 0.11.2-preview | 74 | 8/7/2026 |
| 0.11.1-preview | 70 | 8/7/2026 |
| 0.11.0-preview | 70 | 8/6/2026 |
| 0.10.1-preview | 75 | 8/6/2026 |
| 0.10.0-preview | 59 | 8/6/2026 |
| 0.9.1-preview | 75 | 8/4/2026 |
| 0.9.0-preview | 61 | 8/4/2026 |
| 0.8.0-preview | 63 | 8/4/2026 |
| 0.7.2-preview | 68 | 7/31/2026 |
| 0.7.1-preview | 56 | 7/30/2026 |