# HashiCorp Vault / OpenBao

Optional Socigy.OpenSource.DB.HashiCorp package: field encryption keyed from HashiCorp Vault or OpenBao and rotating PostgreSQL credentials from the Database secrets engine, wired with one DI call each.

The optional **`Socigy.OpenSource.DB.HashiCorp`** package integrates Socigy with [HashiCorp Vault](https://www.vaultproject.io/) — or the API-compatible [OpenBao](https://openbao.org/) fork — for two things:

1. **Field encryption.** It supplies the `IFieldEncryptor` for [`[Encrypted]` columns](/database/0.3.4/defining-models/encrypted-columns), keyed from Vault.
2. **Rotating DB credentials.** It supplies an `IDbCredentialsProvider` that leases short-lived PostgreSQL credentials from Vault's Database secrets engine, which the generated connection factory consumes automatically.

```bash
dotnet add package Socigy.OpenSource.DB.HashiCorp
```

> **NOTE** This package depends on the main `Socigy.OpenSource.DB` package and `VaultSharp`. The two Vault features are independent: register either or both.

> **NOTE** [**OpenBao**](https://openbao.org/) — the open-source fork of Vault — works as a drop-in. Point the same options (`Address`, `Token`/`AppRole`) at your OpenBao server; its KV-v2, Transit and Database secrets-engine APIs are wire-compatible. The integration test suite passes against both Vault and OpenBao.

## Field encryption

The package supplies the ambient `IFieldEncryptor` for [`[Encrypted]` columns](/database/0.3.4/defining-models/encrypted-columns) in one of three modes. Each installs at host start, so once registered every `[Encrypted]` column just works.

### KV-direct — local key from a KV-v2 secret

The data-encryption key is read from a **KV-v2 secret at startup** and used for fast, local AES-256-CBC + HMAC encryption. Per-field crypto stays synchronous with no round-trip per row. Store a Base64-encoded 32-byte key:

```bash
vault kv put secret/socigy/db-encryption-key key="$(openssl rand -base64 32)"
```

```csharp
builder.Services.AddSocigyVaultEncryption(o =>
{
    o.Address = "https://vault.example.com:8200";
    o.Token = builder.Configuration["Vault:Token"];     // or AppRoleId + AppRoleSecretId
    o.KvMountPoint = "secret";
    o.KeySecretPath = "socigy/db-encryption-key";
    o.KeyField = "key";
});
```

> **NOTE** Rotating a KV-direct key means updating the KV secret and re-encrypting existing rows, because the ciphertext carries no key identifier. For rotation without re-encryption, use the envelope mode below.

### Transit data-key envelope — rotation without re-encryption (recommended)

Vault's **Transit** engine holds a key-encryption key; the package keeps a **versioned keyring of Transit-wrapped data-encryption keys (DEKs)** in a KV-v2 secret. At startup (and on rotation) the DEKs are unwrapped via Transit into memory, and per-field crypto stays **local** — Transit is only contacted at startup and rotation, never per row. Each encrypted value embeds the id of the DEK that produced it, so after a rotation **old rows stay readable** (their DEK is retained) while new writes use the newest DEK.

Enable Transit and create the key (it must **not** be derived — the package generates random DEKs from it):

```bash
vault secrets enable transit
vault write -f transit/keys/socigy-db
```

```csharp
builder.Services.AddSocigyVaultEnvelopeEncryption(o =>
{
    o.Address = "https://vault.example.com:8200";
    o.AppRoleId = builder.Configuration["Vault:RoleId"];
    o.AppRoleSecretId = builder.Configuration["Vault:SecretId"];
    o.TransitMountPoint = "transit";
    o.TransitKeyName = "socigy-db";
    o.KvMountPoint = "secret";
    o.KeyringSecretPath = "socigy/db-keyring";   // where the wrapped-DEK keyring is stored
    o.EnableBackgroundRotation = true;            // optional; default off
    o.RotationInterval = TimeSpan.FromDays(90);
});
```

Rotation mints a new DEK and advances the current key version. Run it on the interval above, or trigger it manually by resolving the encryptor:

```csharp
await app.Services.GetRequiredService<VaultEnvelopeEncryptor>().RotateAsync();
```

> **NOTE** After a rotation, any row your app re-saves is re-encrypted to the newest key automatically (writes always use the current DEK); untouched rows stay readable via the keyring. To proactively rewrite old rows, see [Key rotation and re-encryption](#key-rotation-and-re-encryption).

### Transit EaaS-direct — per-field Vault encryption

In EaaS-direct mode Vault encrypts and decrypts **each field value directly** (ciphertext is a `vault:vN:…` string), so rotating the Transit key is enough to move new writes forward, and old ciphertext can be upgraded with `transit/rewrap`. This makes a **Vault round-trip per field**, so it suits a few highly-sensitive columns (typically via a [profile](#per-column-profiles)), not bulk scans. The key must be created with `derived=true` so the `table:column` context binds:

```bash
vault write transit/keys/socigy-eaas derived=true
```

```csharp
builder.Services.AddSocigyVaultTransitEncryption(o =>
{
    o.Address = "https://vault.example.com:8200";
    o.Token = builder.Configuration["Vault:Token"];
    o.TransitKeyName = "socigy-eaas";
    o.Profile = "transit";          // route only [Encrypted(Profile = "transit")] columns here
    o.DecryptCacheSize = 10_000;    // bounded in-memory cache to soften repeated reads
});
```

### Per-column profiles

Run one mode as the default and route specific columns to another by giving the registration a `Profile` and marking the column with it. A common setup is local/envelope by default with Transit EaaS for the most sensitive fields:

```csharp
[Encrypted] public string Email { get; set; }                     // default encryptor (e.g. envelope)
[Encrypted(Profile = "transit")] public string Ssn { get; set; }   // EaaS-direct
```

Only the registration whose `Profile` is unset becomes the default; profiled registrations are reached by name. See [Encrypted columns → Profiles](/database/0.3.4/defining-models/encrypted-columns).

### Key rotation and re-encryption

Old rows always stay readable after a rotation (envelope keyring / retained Transit versions), and rows re-saved through normal writes migrate to the current key for free. To **proactively** rewrite old rows — e.g. to retire an old key version — use the byte-level `FieldReencryptor` (in `Socigy.OpenSource.DB.Core.Encryption.Reencryption`). It re-encrypts (envelope) or rewraps (EaaS) without exposing plaintext, and covers generated, dynamic, and `[TableType]` tables:

```csharp
await new FieldReencryptor()
    .Add<User>()                              // statically-named generated table
    .AddDynamic<Event>("events_2026_06")      // dynamic / [TableType] table bound to a runtime name
    .RunAsync(connection, new ReencryptOptions { BatchSize = 500, DryRun = false });
```

It is keyset-paginated and transactional per batch (so it is resumable), skips values already at the current version, and reports the rows scanned and cells upgraded per table.

## Rotating database credentials

Configure Vault's Database secrets engine with a role per database, then map each Socigy database name to its Vault role:

```csharp
builder.Services.AddSocigyVaultCredentials(o =>
{
    o.Address = "https://vault.example.com:8200";
    o.AppRoleId = builder.Configuration["Vault:RoleId"];
    o.AppRoleSecretId = builder.Configuration["Vault:SecretId"];
    o.DatabaseMountPoint = "database";
    o.BaseConnectionString = "Host=db.internal;Port=5432;Pooling=true";   // no user/pass
    o.DatabaseRoles["AuthDb"] = "auth-db-role";
    o.DatabaseRoles["UserDb"] = "user-db-role";
    o.RefreshInterval = TimeSpan.FromMinutes(30);   // fallback only; renewal normally tracks the lease TTL
});

builder.AddAuthDb();   // the generated factory picks up IDbCredentialsProvider automatically
builder.AddUserDb();
```

How it fits together:

- At startup the package leases credentials for each configured database and composes a connection string (`BaseConnectionString` + the leased `Username`/`Password`), cached in memory. The string is built with `DbConnectionStringBuilder`, so leased passwords containing `;`, `=`, quotes or spaces are escaped correctly.
- The generated connection factory calls the provider **synchronously** for the current connection string each time it opens a connection. See [Connections & DI](/database/0.3.4/core-concepts/connections-and-di).
- A background timer renews credentials at **~2/3 of the actual lease TTL** (`LeaseDurationSeconds` returned by Vault), so they are refreshed before they expire even when the role issues short leases. `RefreshInterval` is only a fallback used when the lease TTL is unknown. When credentials rotate, new connections use the new string and Npgsql's old pool drains naturally.

## Authentication

Both `Add…` calls accept either a **token** (`Token`) or **AppRole** (`AppRoleId` + `AppRoleSecretId`). AppRole is recommended for production workloads.

The Vault **auth token is kept alive automatically** by a background service: it renews the token (renew-self) before it expires, and when renewal can no longer extend it (max TTL) and AppRole credentials are configured, it re-logs-in for a fresh token. A **static, non-renewable token cannot be kept alive** — for long-running services use AppRole, a periodic token, or a renewable token. The library logs a clear error if it detects a token it cannot keep alive.

> **WARNING** Use an `https://` Vault `Address` in production. If the address is plaintext `http://` to a non-loopback host, the library logs a warning — tokens, keys, and leased credentials would otherwise travel unencrypted. Plain `http://` to loopback is fine for local development.

## Diagnostics

All background actions are observable so admins can track what the library does:

- **OpenTelemetry spans** under the existing `Socigy.OpenSource.DB` ActivitySource: `vault.encryption.key.fetch` and `vault.encryption.key.rotate` (key load / rotation), the `vault.transit.*` spans for envelope and EaaS Transit calls (`datakey`, `unwrap`, `encrypt`, `decrypt`, `rewrap`, `key.read`), `socigy.db.encryption.reencrypt` for a bulk re-encryption pass, `vault.credentials.lease` (with `db.name`, `vault.database.role`, and lease-duration tags), and `vault.token.renew` / `vault.token.relogin` for background auth-token upkeep. Subscribe with `AddSource("Socigy.OpenSource.DB")` (see [Diagnostics & OpenTelemetry](/database/0.3.4/observability/diagnostics)).
- **`ILogger` messages** under categories `Socigy.OpenSource.DB.Vault.Encryption`, `Socigy.OpenSource.DB.Vault.Rotation`, and `Socigy.OpenSource.DB.Vault.Credentials`: key/keyring load, rotation, each credential lease (database, role, user, lease seconds), renewal ticks, and failures. The connection factory also logs when it refreshes rotating credentials, and `SocigyFieldEncryption.Configure` logs when an encryptor is installed. EaaS-direct mode logs a one-time warning that every field read/write is a Vault round-trip.
