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(...) and WithIdentityUrl(...) override the Bitwarden API and identity endpoints. Accepts a string, a parameter, an ExternalServiceResource, or an EndpointReference. 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}.json relative 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:

  1. Bitwarden upstream — if the secret already exists in Bitwarden, its current value is synced automatically. No prompt, no configuration needed. In aspire deploy, the bitwarden-pre-sync-managed-{name} step writes this value to the deployment state before process-parameters runs, so the deploy command does not prompt either.
  2. Configuration — if no upstream value is found, the secret reads the configuration key Parameters:{bitwardenResourceName}-{secretName} (e.g. Parameters:bitwarden-api-key).
  3. 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:

  • OrganizationId
  • ProjectId
  • AccessToken
  • ApiUrl
  • IdentityUrl

Deployment

Deployment applies your declared Bitwarden resources.

Typical flow:

  1. Declare the Bitwarden project and any managed secrets in the AppHost graph.
  2. Run aspire deploy for the AppHost.

During aspire deploy, the integration runs six pipeline steps per Bitwarden resource:

  1. 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-parameters runs. This prevents aspire deploy from re-prompting for secrets that already exist in Bitwarden.
  2. Authenticate — resolves credentials and authenticates with Bitwarden Secrets Manager.
  3. Provision project — creates or updates the remote Bitwarden project.
  4. Sync managed secrets — reads existing upstream values for managed secrets whose local parameter values are missing.
  5. Provision secrets — creates or updates managed secrets and validates declared references.
  6. 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: WithBitwardenAuthCacheVolume requires 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. ~/bitwarden or C:\Users\dev\bitwarden) to the string overload. Unlike WithBindMount, 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:

  1. 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.
  2. Upstream managed secret sync — resolves the project and reads existing Bitwarden values for managed secrets whose local parameter values are missing.
  3. 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.
  4. Parameter collection — waits for any remaining project name, organization ID, and managed secret values. The resource enters Running 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 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

.NET 8.0

.NET 9.0

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