Vellum.Rotation
0.4.2
dotnet add package Vellum.Rotation --version 0.4.2
NuGet\Install-Package Vellum.Rotation -Version 0.4.2
<PackageReference Include="Vellum.Rotation" Version="0.4.2" />
<PackageVersion Include="Vellum.Rotation" Version="0.4.2" />
<PackageReference Include="Vellum.Rotation" />
paket add Vellum.Rotation --version 0.4.2
#r "nuget: Vellum.Rotation, 0.4.2"
#:package Vellum.Rotation@0.4.2
#addin nuget:?package=Vellum.Rotation&version=0.4.2
#tool nuget:?package=Vellum.Rotation&version=0.4.2
Vellum
Vellum (n.) — fine parchment historically used for important documents, charters, and sealed letters. Evokes preservation, authenticity, and secrecy — the core values of envelope encryption.
Vellum is a provider-agnostic, production-grade envelope encryption library for .NET with DEK lifecycle management, key rotation, and multi-tenant support.
Status:
0.2.0— Pre-1.0 (API may evolve, breaking changes allowed per semver; see CHANGELOG for the 0.2.0 breaking changes). Production use is supported but cloud KMS providers (Azure Key Vault, AWS KMS, GCP KMS) are planned for 0.3.0.
What is envelope encryption?
Envelope encryption is a pattern where:
- A Data Encryption Key (DEK) encrypts the actual payload (via AES-GCM).
- A Key Encryption Key (KEK), managed by a secure provider (Vault, KMS, HSM), wraps the DEK.
- Only the wrapped DEK is stored alongside the ciphertext.
- Decryption requires access to the KEK provider to unwrap the DEK.
This pattern lets you rotate keys at the KEK level without re-encrypting every payload, and ensures that a database dump alone is not sufficient to decrypt data.
Packages
| Package | Purpose |
|---|---|
Vellum.Abstractions |
Interfaces + value objects. Zero dependencies. |
Vellum.Core |
DekManager + PayloadEncryptor. AES-GCM via System.Security.Cryptography. |
Vellum.Vault |
HashiCorp Vault Transit KEK provider. Token + AppRole auth, built-in HTTP resilience. |
Vellum.AzureKeyVault |
Azure Key Vault KEK provider (Phase 3). |
Vellum.AwsKms |
AWS KMS KEK provider (Phase 3). |
Vellum.GcpKms |
Google Cloud KMS KEK provider (Phase 3). |
Vellum.Static |
Static key provider for dev/test only — INSECURE. |
Vellum.EntityFrameworkCore |
EF Core-backed key store. |
Vellum.InMemory |
In-memory key store for tests. |
Vellum.Rotation |
Opt-in background DEK rotation hosted service. |
Vellum.AspNetCore |
DI extensions + health checks (Phase 4). |
Quickstart
Install the packages:
dotnet add package Vellum.Abstractions
dotnet add package Vellum.Core
dotnet add package Vellum.Vault
dotnet add package Vellum.EntityFrameworkCore
Wire Vellum into an ASP.NET Core / generic host app. Options for the EF Core store
flow via UseVellum on the DbContextOptionsBuilder — configure them once, no duplication:
// Program.cs
services.AddVellum(o => o.DekCacheTtl = TimeSpan.FromMinutes(30));
services.AddVaultProvider(o =>
{
o.Address = "https://vault:8200"; // plain http:// is rejected by default
o.Token = builder.Configuration["Vault:Token"]!;
o.KeyName = "my-kek";
// Production: prefer AppRole over a static token — tokens are then re-acquired
// automatically before they expire (o.AuthMethod = VaultAuthMethod.AppRole
// + o.RoleId / o.SecretId). Requests are retried on transient failures by
// default (o.EnableResilience = false to opt out).
});
services.AddDbContext<AppDbContext>(options => options
.UseNpgsql(builder.Configuration.GetConnectionString("App"))
.UseVellum(v => v.UniqueActiveIndexFilter = "\"IsActive\" = true"));
services.AddEntityFrameworkCoreStore<AppDbContext>();
// AppDbContext.cs
public sealed class AppDbContext(DbContextOptions<AppDbContext> opts) : DbContext(opts)
{
protected override void OnModelCreating(ModelBuilder mb)
{
base.OnModelCreating(mb);
mb.AddVellumEncryptionKeys(this);
}
}
// Usage (inject IPayloadEncryptor anywhere)
EncryptedPayload envelope = await encryptor.EncryptStringAsync("hello", scope: "tenant:42");
string roundtrip = await encryptor.DecryptStringAsync(envelope, scope: "tenant:42");
The envelope carries its own wrapped DEK, so its own fields always suffice to decrypt it.
Since 0.4.0 the decrypt resolves the DEK from the key store by KeyId first and uses that
embedded copy as the fallback — see the 0.4.0 changelog entry for what that
buys (rewrapping one store record per DEK instead of every stored row) and what it costs (a
decrypt normally touches the store). By default the ciphertext is also bound to its scope via AES-GCM
associated data (envelope format version 2): decrypting requires the same scope, so an
envelope copied between tenants fails the authentication tag check instead of decrypting.
Opt out with VellumOptions.BindScopeToCiphertext = false if the scope is genuinely
unavailable at decrypt time; consumers persisting envelopes field-by-field must persist
EncryptedPayload.FormatVersion alongside the other fields. DecryptAsync is backed by
a Vellum-private in-memory cache keyed by the wrapped ciphertext (see
#6), so read-heavy workloads pay at most
one KEK round-trip per distinct DEK.
For more complete wiring — feature-flagged rollouts, snake_case schemas, migrations
from a legacy encryption layer, appsettings.json bridging — see the
samples/ folder and docs/consumer-options-bridging.md.
Background DEK rotation (Vellum.Rotation)
Rotation is opt-in — Vellum.Core never starts a background service. Add the
Vellum.Rotation package and register the worker:
services.AddVellumRotation(o =>
{
o.RotationInterval = TimeSpan.FromHours(24); // how often the worker ticks
o.MaxDekAge = TimeSpan.FromHours(24); // rotate only keys at least this old
o.MaxRetriesPerScope = 3; // retry with backoff inside the tick
});
Each tick rotates only the scopes whose active DEK has reached MaxDekAge; transient
KEK-provider failures are retried per scope with exponential backoff and jitter inside
the tick, and a failed scope never blocks the others. Rotation is fail-safe: if the KEK
provider fails mid-rotation, the previous key stays active. KEK rewrap tooling
(VellumRewrapService.RewrapStoredKeysAsync + IPayloadEncryptor.RewrapPayloadAsync)
re-encrypts stored keys and persisted envelopes under the current KEK version, making it
safe to retire old KEK versions. For the full operational picture — including KEK rotation
on the Vault side and the min_decryption_version procedure — see the
key rotation runbook.
Design-time scaffolder (dotnet ef migrations add)
dotnet ef prefers an IDesignTimeDbContextFactory<T> over the host-build path when
both are available. A factory that forgets to call UseVellum(...) produces
DbContextOptions without the Vellum options extension, so the scaffolder generates a
migration against Vellum's defaults (PascalCase column names, SQL Server filter
syntax) even if the runtime AddDbContext pipeline is configured correctly.
This is issue #17.
The recommended fix is to declare the Vellum schema once on
the DbContext class with [VellumEntityFrameworkOptions]. The attribute is
reflection-readable at both runtime and design time, so the scaffolder picks up the
overrides with no duplication between Program.cs and the factory:
[VellumEntityFrameworkOptions(
TableName = "bus_encryption_keys",
KeyIdColumnName = "key_id",
ScopeColumnName = "scope",
WrappedCiphertextColumnName = "wrapped_ciphertext",
WrappedProviderVersionColumnName = "wrapped_provider_version",
CreatedAtColumnName = "created_at",
ExpiresAtColumnName = "expires_at",
IsActiveColumnName = "is_active",
UniqueActiveIndexFilter = "\"is_active\" = true")]
public sealed class AppDbContext(DbContextOptions<AppDbContext> opts) : DbContext(opts)
{
protected override void OnModelCreating(ModelBuilder mb)
{
base.OnModelCreating(mb);
mb.AddVellumEncryptionKeys(this);
}
}
The attribute resolution order is: (1) UseVellum(...) on the options builder, (2)
[VellumEntityFrameworkOptions] on the context type, (3)
IOptions<VellumEntityFrameworkOptions> from the application service provider, (4)
defaults. Every attribute property the consumer leaves unset keeps the Vellum
default, so a minimal decoration like
[VellumEntityFrameworkOptions(TableName = "bus_encryption_keys")] is valid.
As an alternative, an IDesignTimeDbContextFactory<T> can call UseVellum(...)
explicitly alongside the provider extension:
public sealed class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<AppDbContext> b = new();
b.UseNpgsql("Host=localhost;Database=app;Username=postgres;Password=postgres");
b.UseVellum(v => v.UniqueActiveIndexFilter = "\"IsActive\" = true");
return new AppDbContext(b.Options);
}
}
Either pattern works. The attribute is cleaner when the Vellum schema is fixed at compile time; the factory pattern is more flexible when options come from a configuration source the factory can read directly.
Building locally
Requires the .NET 10 SDK (see global.json) plus the .NET 8 and .NET 9 runtimes for running the multi-target test suite.
dotnet restore --locked-mode
dotnet build -c Release
dotnet test -c Release
--locked-mode is mandatory: Vellum commits packages.lock.json for every project so transitive versions are pinned and auditable. Any drift fails the restore.
Design principles
- Provider-agnostic — Vault, AWS KMS, Azure Key Vault, GCP KMS, static (dev) are first-class.
- Storage-agnostic — EF Core by default, but any storage can implement
IEncryptionKeyStore. - No custom crypto — exclusively AES-GCM via
System.Security.Cryptography. - Multi-tenant by construction — keys scoped via an opaque
Scopestring. - Fail closed — any error in key resolution, wrap, or unwrap throws. Never silently degrades.
- Zero allocation where possible —
Span<byte>/Memory<byte>on the hot path. - Testable — every interface mockable; in-memory provider + store for unit tests.
- No background services in Core — rotation is opt-in via
Vellum.Rotation.
Security
Please see SECURITY.md for responsible disclosure and threat model.
License
Apache License 2.0. See LICENSE.
| Product | Versions 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 is compatible. 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 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Options (>= 9.0.17)
- Vellum.Abstractions (>= 0.4.2)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Options (>= 9.0.17)
- Vellum.Abstractions (>= 0.4.2)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.17)
- Microsoft.Extensions.Options (>= 9.0.17)
- Vellum.Abstractions (>= 0.4.2)
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.4.2 | 102 | 9/28/2026 |
| 0.4.1 | 109 | 9/25/2026 |
| 0.4.0 | 110 | 9/25/2026 |
| 0.2.0 | 178 | 7/3/2026 |
| 0.0.1-reserved | 128 | 4/11/2026 |