CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager 13.4.0-preview.3
CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager
Overview
Integrates Bitwarden Secrets Manager into your Aspire AppHost. Declare your Bitwarden project and secrets in the AppHost graph and apply them with aspire deploy.
Getting Started
Install the package
dotnet add package CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager
Basic setup
IResourceBuilder<ParameterResource> projectNameOrId = builder.AddParameter("bitwarden-project");
IResourceBuilder<ParameterResource> organizationId = builder.AddParameter("bitwarden-organization-id");
IResourceBuilder<ParameterResource> accessToken = builder.AddParameter("bitwarden-access-token", secret: true);
IResourceBuilder<BitwardenSecretManagerResource> bitwarden = builder.AddBitwardenSecretManager(
"bitwarden",
projectNameOrId,
organizationId,
accessToken);
The projectNameOrId parameter accepts either a project name (creates or finds the project by name) or a project identifier GUID (adopts the existing project by ID without renaming it). The routing is automatic: if the resolved value parses as a GUID it is treated as an ID, otherwise it is treated as a name.
Use WithApiUrl and WithIdentityUrl to override the Bitwarden endpoints. Both default to the public Bitwarden cloud. For a self-hosted instance, pass an ExternalServiceResource to set the URL and wire up WaitFor in one call:
var bitwardenApiServer = builder.AddExternalService("bitwarden-api", "https://bitwarden.example.com/api")
.WithHttpHealthCheck("/alive");
var bitwardenIdentityServer = builder.AddExternalService("bitwarden-identity", "https://bitwarden.example.com/identity")
.WithHttpHealthCheck("/alive");
bitwarden
.WithApiUrl(bitwardenApiServer)
.WithIdentityUrl(bitwardenIdentityServer);
When the URL varies by environment, use a parameter instead:
var bitwardenApiUrl = builder.AddParameter("bitwarden-api-url");
var bitwardenApiServer = builder.AddExternalService("bitwarden-api", bitwardenApiUrl)
.WithHttpHealthCheck("/alive");
bitwarden.WithApiUrl(bitwardenApiServer);
Use WithCacheFile to override the AppHost cache location. The cache tracks Bitwarden project and secret IDs between runs. Default is .bitwarden/{name}.{env}.json relative to the AppHost directory; relative paths resolve from there.
bitwarden.WithCacheFile(".bitwarden/shared.Development.json");
Use WithAuthCacheDirectory to override the AppHost auth cache location. The auth cache persists the Bitwarden SDK session between runs to avoid login rate-limiting. Default is the Aspire store, named by the token UUID; relative paths resolve from there.
bitwarden.WithAuthCacheDirectory("/ci/bitwarden-auth");
Managed secrets
Use AddSecret(...) to declare AppHost-owned secrets. Aspire creates the secret if it does not exist and updates it when the local value changes.
// Aspire resource name and Bitwarden secret name are the same
IResourceBuilder<BitwardenSecretResource> managedSecret = bitwarden.AddSecret("api-key");
// Aspire resource name and Bitwarden secret name differ
IResourceBuilder<BitwardenSecretResource> managedSecret = bitwarden.AddSecret("api-key", remoteName: "API Key");
The value is resolved in this order during startup:
- Bitwarden upstream — if the secret already exists, its current value is synced automatically. No prompt or configuration needed.
- Configuration — reads
Parameters:{bitwardenResourceName}-{secretName}(e.g.Parameters:bitwarden-api-key). - Interactive prompt — the dashboard prompts for the value. Once supplied, Bitwarden creates the secret.
Aspire finds or creates the secret entirely by name and cached ID. There is no explicit GUID adoption for managed secrets — if the same secret was created in a previous run it will be found automatically.
Externally managed secrets
Use GetSecret(...) to reference a secret that already exists in Bitwarden and is owned outside the AppHost. Aspire reads the value but never writes to it.
// Aspire resource name and Bitwarden secret name are the same
IResourceBuilder<BitwardenSecretResource> existingSecret = bitwarden.GetSecret("api-key");
// Aspire resource name and Bitwarden secret name differ
IResourceBuilder<BitwardenSecretResource> existingSecret = bitwarden.GetSecret("api-key", remoteName: "API Key");
// Multiple secrets share the same name — Bitwarden does not enforce name uniqueness,
// so use the GUID to identify the specific secret
IResourceBuilder<BitwardenSecretResource> existingSecret = bitwarden.GetSecret("api-key", Guid.Parse("00000000-0000-0000-0000-000000000000"));
Injecting secrets into dependent resources
Use WithReference(...) to inject Bitwarden client configuration into dependent resources.
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden);
The injected configuration is under Aspire:Bitwarden:SecretManager:{connectionName} and includes OrganizationId, ProjectId, AccessToken, ApiUrl, and IdentityUrl.
By default the management token is injected. To supply a least-privilege read-only token instead:
IResourceBuilder<ParameterResource> readOnlyToken = builder.AddParameter("bitwarden-readonly-token", secret: true);
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden)
.WithBitwardenAccessToken(bitwarden, readOnlyToken)
.WithBitwardenAuthCacheDirectory(bitwarden, "/data/bitwarden"); // optional, use if you encounter login rate limits
Note: The read-only token must be granted read permissions to the Bitwarden project manually — Bitwarden does not expose an API for this. Do this after the first AppHost run that creates the project.
To inject a secret ID for runtime fetching via the Bitwarden SDK:
IResourceBuilder<BitwardenSecretResource> managedSecret = bitwarden.AddSecret("demo-api-key");
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden)
.WaitForCompletion(bitwarden)
.WithEnvironment("DEMO_API_KEY_SECRET_ID", managedSecret.AsSecretId());
To inject the resolved value directly (no SDK required in the app, but requires redeploy when the value changes):
builder.AddProject<Projects.ApiService>("api")
.WaitForCompletion(bitwarden)
.WithEnvironment("DEMO_API_KEY", managedSecret);
Deployment
Run aspire deploy. The integration adds six pipeline steps per Bitwarden resource:
- Pre-sync managed secrets — authenticates and fetches existing Bitwarden values for managed secrets before
process-parametersruns. Prevents re-prompting for secrets that already exist. - Authenticate — resolves credentials and authenticates with Bitwarden Secrets Manager.
- Provision project — creates or updates the remote Bitwarden project.
- Sync managed secrets — reads upstream values for managed secrets whose local parameter values are missing.
- Provision secrets — creates or updates managed secrets and validates declared references.
- Patch env files — applies resolved values to Docker Compose environment files (Docker Compose deployments only).
Reference
Access tokens
| Token | Set with | Used by | Permissions needed | When to use |
|---|---|---|---|---|
| Management token | AddBitwardenSecretManager(..., projectNameOrId, ..., accessToken) |
AppHost reconciler | Read + write to project | Always required |
| Client token | WithBitwardenAccessToken(bitwarden, token) |
Deployed app | Read-only to project | Supply a least-privilege token so the deployed app cannot modify secrets |
Secret declarations
Both return IResourceBuilder<BitwardenSecretResource>. Pass the builder directly to WithEnvironment to inject the resolved secret value, or call .AsSecretId() on the builder to inject the secret ID instead.
| API | Ownership | Bitwarden writes | When to use |
|---|---|---|---|
AddSecret(name) |
AppHost | Yes (upsert) | Both names are the same |
AddSecret(name, remoteName) |
AppHost | Yes (upsert) | Aspire and Bitwarden names differ |
GetSecret(name) |
External | No | Both names are the same |
GetSecret(name, remoteName) |
External | No | Aspire and Bitwarden names differ |
GetSecret(name, secretId) |
External | No | Multiple secrets share the same name (Bitwarden does not enforce uniqueness) |
Secret references (injected into dependent resources)
| API | What it injects | When to use |
|---|---|---|
WithReference(bitwarden) |
Connection config (OrganizationId, ProjectId, AccessToken, ApiUrl, IdentityUrl) |
App uses the Bitwarden SDK to read secrets at runtime |
WithBitwardenAccessToken(bitwarden, token) |
Overrides the injected access token for this connection | Supply a least-privilege read-only token |
WithEnvironment(envVar, secret.AsSecretId()) |
Injects a secret ID as an env var; app fetches the value via the SDK at runtime | Dynamic secret retrieval without redeploying when values change |
WithBitwardenAuthCacheDirectory(bitwarden, dir) |
Configures the app's Bitwarden SDK auth cache directory for this connection | Persist auth session across restarts (process resources) |
WithBitwardenAuthCacheVolume(bitwarden) |
Mounts a named volume as the auth cache for this connection | Persist auth session across restarts (container resources) |
WithEnvironment(envVar, secret) |
Injects the resolved secret value as an env var | Simple injection; no Bitwarden SDK needed in the app |
Cache files
| Cache | Format | Stores | Default | Override | Relative paths | When to override |
|---|---|---|---|---|---|---|
| AppHost cache | JSON (integration-managed) | Project ID + secret ID mappings | .bitwarden/{name}.{env}.json relative to AppHost directory |
bitwarden.WithCacheFile(path) |
AppHost directory | Share cache across AppHost projects or CI pipelines |
| AppHost auth cache | Encrypted (Bitwarden SDK-managed) | AppHost Bitwarden SDK session | Aspire store, named by token UUID | bitwarden.WithAuthCacheDirectory(path) |
Aspire store | Share session across CI runs |
| App auth cache | Encrypted (Bitwarden SDK-managed) | App Bitwarden SDK session | Not set — app re-authenticates each start | WithBitwardenAuthCacheVolume(bitwarden) (containers) or WithBitwardenAuthCacheDirectory(bitwarden, dir) (processes) |
— | Persist app session across restarts |
App auth cache
Without an auth cache the app re-authenticates with Bitwarden on every start, which triggers rate limiting under frequent restarts or rolling deployments.
Named volume (containers)
WithBitwardenAuthCacheVolume mounts a named volume and injects the path automatically. The volume survives restarts and is provisioned by the deploy tooling.
builder.AddContainer("api", "myregistry/api")
.WithReference(bitwarden)
.WithBitwardenAuthCacheVolume(bitwarden); // volume: api-bitwarden-bitwarden-auth, path: /var/lib/bitwarden
Override the volume name or mount path when needed:
api.WithBitwardenAuthCacheVolume(bitwarden, volumeName: "shared-bw-auth", containerDirectory: "/var/lib/bitwarden-shared");
Note:
WithBitwardenAuthCacheVolumerequires a container resource and throws at startup for process resources (e.g.AddProject).
Parameter (directory varies by environment)
Use a parameter when the path differs between dev and production.
IResourceBuilder<ParameterResource> authCacheDir = builder.AddParameter("bw-auth-cache-dir");
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden)
.WithBitwardenAuthCacheDirectory(bitwarden, authCacheDir);
Set the directory in user secrets for local development:
{
"Parameters": {
"bw-auth-cache-dir": "/home/dev/.bitwarden"
}
}
Fixed string (same path everywhere)
Use a string literal only when the app always runs as a container and the path is the same in all environments.
builder.AddContainer("api", "myregistry/api")
.WithReference(bitwarden)
.WithBitwardenAuthCacheDirectory(bitwarden, "/home/app/.bitwarden");
Warning: Do not pass a host-specific path to the string overload — the value is injected as-is and silently breaks in a container. Use a parameter when the path differs between machines or modes.
When to use each
| Scenario | API |
|---|---|
| App is a Docker container and you want persistent auth across restarts | WithBitwardenAuthCacheVolume(bitwarden) |
| App runs as a process in dev and as a container in production, dirs differ | WithBitwardenAuthCacheDirectory(bitwarden, parameterBuilder) |
| App is always a container and the directory is the same everywhere | WithBitwardenAuthCacheDirectory(bitwarden, string) |
App is a process resource (AddProject) |
WithBitwardenAuthCacheDirectory(bitwarden, string) or parameter — no volume option |
Resource states
The Bitwarden resource is a one-shot provisioner. Dependent resources must call .WaitForCompletion(bitwarden) explicitly to block until it reaches Finished.
Provisioning runs in four phases before Running:
- Authentication — waits for the management access token, then authenticates. Fails fast so a bad token surfaces before you supply remaining values.
- Upstream managed secret sync — resolves the project and reads existing Bitwarden values for managed secrets whose local parameter values are missing.
- Upstream reference secret sync — fetches values for all
GetSecretsecrets. Fails here if a referenced secret does not exist in Bitwarden. - Parameter collection — waits for any remaining project name, organization ID, and managed secret values.
Runningis entered only once every value is in hand.
| State | Style | Dependent resources |
|---|---|---|
NotStarted |
— | Blocked |
Waiting |
— | Blocked |
Running |
— | Blocked (still provisioning) |
Finished |
Success | Unblocked — start normally |
Exited (exit code 1) |
Error | Error — fail to start |
Project provisioning decisions
Runs once per AppHost run during bitwarden-provision-project. Paths tried in order: ID-based adoption → persisted mapping → create new.
Path A — ID-based adoption (projectNameOrId resolves to a GUID)
| Found in Bitwarden | Outcome |
|---|---|
| ✓ | Use configured project |
| ✗ | Error: configured project not found |
Path B — persisted mapping exists in cache
| Found in Bitwarden | Name matches configured | Outcome |
|---|---|---|
| ✓ | ✓ | Reuse persisted project |
| ✓ | ✗ | ⚠ Update project name (name drifted) |
| ✗ | — | ⚠ Create new project (persisted ID gone) |
Path C — no cache
Create new project. To adopt a project created outside the declared graph, set projectNameOrId to its GUID.
Managed secret provisioning decisions
Runs once per AddSecret secret during bitwarden-provision-secrets. Paths tried in order: persisted mapping → name search → create new.
Path A — persisted mapping exists in cache
| Secret found | In project | Outcome |
|---|---|---|
| ✓ | ✓ | Sync secret |
| ✓ | ✗ | ⚠ Create replacement secret |
| ✗ | — | ⚠ Create replacement secret |
Path B — name search
| Name matches | Historical rename | Outcome |
|---|---|---|
| 0 | — | Create new secret |
| 1 | ✗ | Sync secret |
| 1 | ✓ | ⚠ Create new secret (local identity changed) |
| > 1 | — | Prompt user to pick one (error if non-interactive) |
Externally managed secret resolution
Runs once per GetSecret secret during bitwarden-provision-secrets. Read-only — no writes, no cache, no interactive prompt. Paths tried in order: explicit GUID → name search.
Path A — explicit GUID (GetSecret(name, secretId))
| Secret found | In project | Outcome |
|---|---|---|
| ✓ | ✓ | Sync secret value |
| ✓ | ✗ | Error: secret not in project |
| ✗ | — | Error: configured secret not found |
Path B — name search (GetSecret(name) or GetSecret(name, remoteName))
| Name matches | Outcome |
|---|---|
| 0 | Error: secret not found |
| 1 | Sync secret value |
| > 1 | Error: Bitwarden does not enforce name uniqueness — use GetSecret(name, secretId) to target one |
Audit trail
Every time a managed secret is created or updated, the provisioner prepends a timestamped entry to the Bitwarden note field:
[2026-05-29T12:34:56Z] value changed (previous: old-value)
[2026-05-28T09:00:00Z] key renamed (previous: old-key), value changed (previous: initial-value)
[2026-05-27T08:00:00Z] Created
Each entry lists all fields that changed and their previous values. The trail is visible in the Bitwarden web vault and CLI alongside the current secret value.
Compatibility
Tested with Aspire 13.3.0.
This integration relies on several experimental Aspire APIs (ASPIREATS001, ASPIREPIPELINES001/002/004, ASPIREINTERACTION001) and four UnsafeAccessor workarounds against private members of ParameterResource and ParameterProcessor. See ASPIRE-INTERNALS.md for the full explanation of each one, why no public API covers it, and what breaks when Aspire changes it.
No packages depend on CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager.
.NET 10.0
- Aspire.Hosting (>= 13.4.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
.NET 8.0
- Aspire.Hosting (>= 13.4.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
.NET 9.0
- Aspire.Hosting (>= 13.4.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
| Version | Downloads | Last updated |
|---|---|---|
| 13.4.0-preview.3 | 5 | 06/06/2026 |
| 13.3.1-preview.2 | 8 | 05/31/2026 |
| 13.3.1-preview.1 | 1 | 05/31/2026 |