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.1.0— First stable drop. Pre-1.0 (API may evolve in 0.2.0+, breaking changes allowed per semver). Production use is supported but cloud KMS providers (Azure Key Vault, AWS KMS, GCP KMS) are planned for 0.3.0.
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.
| 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. |
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 (Phase 4). |
Vellum.AspNetCore |
DI extensions + health checks (Phase 4). |
Install the packages:
dotnet add package Vellum.Abstractions
dotnet add package Vellum.Core
dotnet add package Vellum.Vault
dotnet add package Vellum.EntityFrameworkCoreWire 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 = "http://vault:8200";
o.Token = builder.Configuration["Vault:Token"]!;
o.KeyName = "my-kek";
});
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);The envelope carries its own wrapped DEK, so decryption is self-contained — no second
DB round-trip. DecryptAsync is backed by an 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.
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.
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.
- 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.
Please see SECURITY.md for responsible disclosure and threat model.
Apache License 2.0. See LICENSE.