CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager 13.3.1-preview.2
CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager
Overview
CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager helps you work with
Bitwarden Secrets Manager in your Aspire AppHost.
Use it to define your Bitwarden project and secrets in one place, then apply
them with aspire deploy.
Getting Started
Install the package
dotnet add package CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager
Basic setup
Create parameters for the project name, organization ID, and access token, then add the Bitwarden resource to your AppHost. The Aspire resource name and the Bitwarden project name are independent.
IResourceBuilder<ParameterResource> organizationId = builder.AddParameter("bitwarden-organization-id");
IResourceBuilder<ParameterResource> accessToken = builder.AddParameter("bitwarden-access-token", secret: true);
IResourceBuilder<ParameterResource> projectName = builder.AddParameter("bitwarden-project-name");
IResourceBuilder<BitwardenSecretManagerResource> bitwarden = builder.AddBitwardenSecretManager(
"bitwarden",
projectName,
organizationId,
accessToken);
Optional configuration
You can further customize the resource with the following options:
WithExistingProject(...)adopts an existing Bitwarden project by identifier.WithApiUrl(...)andWithIdentityUrl(...)override the Bitwarden API and identity endpoints. Accepts a string, a parameter, anExternalServiceResource, or anEndpointReference. Both default to the public Bitwarden cloud and are shown as clickable links in the Aspire dashboard.WithCacheFile(...)overrides the AppHost cache file location (default:.bitwarden/{resourceName}.{environment}.jsonrelative to the AppHost directory). The AppHost cache tracks Bitwarden project and secret IDs between runs. Relative paths are resolved from the AppHost directory.WithAuthCacheDirectory(...)overrides the AppHost auth cache file location (default: Aspire store, named by the UUID embedded in the access token). The AppHost auth cache persists the Bitwarden SDK auth session between runs to avoid hitting Bitwarden's rate limits on frequent logins. Relative paths are resolved from the Aspire store.
For a self-hosted instance, model each endpoint as an ExternalServiceResource
and pass it directly. This sets the URL and wires 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 of a literal string:
var bitwardenApiUrl = builder.AddParameter("bitwarden-api-url");
var bitwardenApiServer = builder.AddExternalService("bitwarden-api", bitwardenApiUrl)
.WithHttpHealthCheck("/alive");
bitwarden.WithApiUrl(bitwardenApiServer);
Usage
Use AddSecret(...) to declare managed Bitwarden secrets.
IResourceBuilder<BitwardenSecretResource> managedSecret = bitwarden.AddSecret("api-key");
Each managed secret appears in the Aspire dashboard parameters tab. Its value is resolved in this order during startup:
- Bitwarden upstream — if the secret already exists in Bitwarden, its current value is
synced automatically. No prompt, no configuration needed. In
aspire deploy, thebitwarden-pre-sync-managed-{name}step writes this value to the deployment state beforeprocess-parametersruns, so the deploy command does not prompt either. - Configuration — if no upstream value is found, the secret reads the configuration key
Parameters:{bitwardenResourceName}-{secretName}(e.g.Parameters:bitwarden-api-key). - Interactive prompt — if the configuration key is also absent, the dashboard prompts for the value. Once supplied, Bitwarden creates the secret with that value.
The dashboard parameter state transitions to Running as soon as the value is resolved by any
of these paths, so the "Parameters need values" banner disappears automatically after the
upstream sync phase completes for existing secrets.
Use GetSecret(...) to reference an existing remote secret.
IResourceBuilder<BitwardenSecretResource> existingSecret = bitwarden.GetSecret("shared-api-key");
Both AddSecret and GetSecret
return IResourceBuilder<BitwardenSecretResource>, the difference is that AddSecret creates a managed secret that is synced and updated by Aspire, while GetSecret is unmanaged (read-only) and must already exist in Bitwarden.
Use WithReference(...) to inject Bitwarden client configuration into
dependent resources.
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden);
By default the management access token is injected into clients. To supply a
least-privilege read-only token instead, chain WithBitwardenAccessToken:
IResourceBuilder<ParameterResource> readOnlyToken = builder.AddParameter("bitwarden-readonly-token", secret: true);
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden)
.WithBitwardenAccessToken(bitwarden, readOnlyToken);
Note: The read-only token must be granted read permissions to the Bitwarden project manually in the Bitwarden web vault or CLI — Bitwarden does not expose an API for this, so it cannot be automated. For a newly created project, do this after the first AppHost run that creates the project.
Chain additional Bitwarden-specific configuration after WithReference:
IResourceBuilder<BitwardenSecretResource> managedSecret = bitwarden.AddSecret("demo-api-key");
// SDK approach: inject connection config + secret ID for runtime fetching
builder.AddProject<Projects.ApiService>("api")
.WithReference(bitwarden)
.WithBitwardenSecretId("DEMO_API_KEY_SECRET_ID", managedSecret.Resource)
.WithBitwardenAuthCacheDirectory(bitwarden, "/data/bitwarden"); // optional
Use WithBitwardenSecretValue(...) to inject the resolved secret value
directly as an environment variable. No Bitwarden SDK required in the app,
but the app must be redeployed when the value changes:
builder.AddProject<Projects.ApiService>("api")
.WithBitwardenSecretValue("DEMO_API_KEY", managedSecret.Resource);
The injected configuration is available under
Aspire:Bitwarden:SecretManager:{connectionName} and includes:
OrganizationIdProjectIdAccessTokenApiUrlIdentityUrl
Deployment
Deployment applies your declared Bitwarden resources.
Typical flow:
- Declare the Bitwarden project and any managed secrets in the AppHost graph.
- Run
aspire deployfor the AppHost.
During aspire deploy, the integration runs six pipeline steps per Bitwarden
resource:
- Pre-sync managed secrets — prompts for any missing credentials, then
authenticates and fetches existing Bitwarden values for managed secrets,
writing everything to the deployment state before
process-parametersruns. This preventsaspire deployfrom re-prompting for secrets that already exist in Bitwarden. - Authenticate — resolves credentials and authenticates with Bitwarden Secrets Manager.
- Provision project — creates or updates the remote Bitwarden project.
- Sync managed secrets — reads existing 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).
This keeps the experience declaration-first: resources and references are your contract, and deployment materializes that contract.
Reference
Access tokens
| Token | Set with | Used by | Permissions needed | When to use |
|---|---|---|---|---|
| Management token | AddBitwardenSecretManager(..., accessToken) |
AppHost reconciler | Read + write to project | Always required |
| Client token | WithReference(bitwarden, bw => bw.WithAccessToken(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>. Access .Resource to pass the secret resource to WithBitwardenSecretValue or WithBitwardenSecretId.
| API | What it does | When to use |
|---|---|---|
AddSecret(name) |
Declares a managed secret — value is written to Bitwarden on every run | When Aspire owns the secret value |
GetSecret(name) |
References an existing remote secret — value is read from Bitwarden, never written | When the secret already exists and you only need to read it |
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 |
WithBitwardenSecretId(envVar, secret) |
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) |
WithBitwardenSecretValue(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
The app auth cache persists the Bitwarden SDK auth session inside the running application. Without it the app calls the Bitwarden identity server on every start, which triggers rate limiting under frequent restarts or rolling deployments. There are three ways to configure it, each suited to a different deployment model.
Named volume (container resources)
Use WithBitwardenAuthCacheVolume when the destination resource is a Docker container. It mounts a
named volume and injects the file path automatically. The volume survives container restarts
and is provisioned by the deploy tooling — no host-specific path is involved.
builder.AddContainer("api", "myregistry/api")
.WithReference(bitwarden)
.WithBitwardenAuthCacheVolume(bitwarden); // volume: api-bitwarden-bitwarden-auth, path: /var/lib/bitwarden
Override the volume name or mount directory when needed:
api.WithBitwardenAuthCacheVolume(bitwarden, volumeName: "shared-bw-auth", containerDirectory: "/var/lib/bitwarden-shared");
Note:
WithBitwardenAuthCacheVolumerequires the destination resource to be a container resource. It throws at startup for process resources (e.g.AddProject).
Parameter (per-environment directory)
Use WithBitwardenAuthCacheDirectory with a parameter when the directory must differ between
environments — for example, a developer machine path in run mode vs. a container-internal
path in production. The parameter reads from user secrets or configuration in run mode,
and the deploy tooling resolves it per environment at deploy time.
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 directory everywhere)
Use WithBitwardenAuthCacheDirectory with a string literal when the directory is a
container-internal path that is identical in all environments. This is appropriate when
the app always runs as a container (not as a DCP process), so there is no host-specific
path involved.
builder.AddContainer("api", "myregistry/api")
.WithReference(bitwarden)
.WithBitwardenAuthCacheDirectory(bitwarden, "/home/app/.bitwarden");
Warning: Do not pass a host-specific directory (e.g.
~/bitwardenorC:\Users\dev\bitwarden) to the string overload. UnlikeWithBindMount, Aspire does not warn about this — the value is injected as-is and will silently break in a container. Use a parameter instead when the directory 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 use WaitForCompletion, so they block until provisioning finishes and then start.
Provisioning runs in four phases before the resource enters Running:
- Authentication — waits only for the management access token, then authenticates with Bitwarden. Fails fast here so you learn about a bad token before providing the 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 reference-only secrets (declared via
GetSecret). 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. The resource enters
Runningonly 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 the bitwarden-provision-project pipeline step.
Paths are tried in order: explicit adoption → persisted mapping → create new.
Path A — explicit adoption (WithExistingProject)
| 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. There is no name-search path here: the AppHost is the source of truth for the project, so a missing cache means a new project is created. Use WithExistingProject to adopt a project that was created outside the declared graph.
Managed secret provisioning decisions
Runs once per managed secret (AddSecret), during the bitwarden-provision-secrets pipeline step.
Paths are tried in order: explicit adoption → persisted mapping → name search.
Path A — explicit adoption (WithExistingSecret)
| Secret found | Outcome |
|---|---|
| ✓ | Sync secret |
| ✗ | Error: configured secret not found |
Path B — persisted mapping exists in cache
| Secret found | In project | Outcome |
|---|---|---|
| ✓ | ✓ | Sync secret |
| ✓ | ✗ | ⚠ Create replacement secret |
| ✗ | — | ⚠ Create replacement secret |
Path C — 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) |
Unmanaged secret resolution
Runs once per unmanaged secret (GetSecret), during the bitwarden-provision-secrets pipeline step.
The value is read from Bitwarden and never written. Paths are tried in order: explicit adoption → name search.
There is no persisted mapping path and no interactive prompt — duplicate names always cause an error.
Path A — explicit adoption (WithExistingSecret)
| Secret found | In project | Outcome |
|---|---|---|
| ✓ | ✓ | Sync secret value |
| ✓ | ✗ | Error: secret not in project |
| ✗ | — | Error: configured secret not found |
Path B — name search
| Name matches | Outcome |
|---|---|
| 0 | Error: secret not found |
| 1 | Sync secret value |
| > 1 | Error: duplicate names (resolve in Bitwarden or adopt by ID with WithExistingSecret) |
Audit trail
Every time a managed secret is created or updated, the provisioner writes or prepends a timestamped entry to its 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
Every change kind records its previous value: key renamed (previous: …), project changed (previous: …), value changed (previous: …). When multiple fields change in a single update, all changes are listed in the same entry.
The audit trail grows at the top of the note on each update. It 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.3.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
.NET 8.0
- Aspire.Hosting (>= 13.3.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
.NET 9.0
- Aspire.Hosting (>= 13.3.0)
- Bitwarden.Secrets.Sdk (>= 1.0.0)
- Polly (>= 8.6.4)
| Version | Downloads | Last updated |
|---|---|---|
| 13.4.0-preview.3 | 4 | 06/06/2026 |
| 13.3.1-preview.2 | 8 | 05/31/2026 |
| 13.3.1-preview.1 | 1 | 05/31/2026 |