|
| 1 | +--- |
| 2 | +title: Bitwarden plugin |
| 3 | +description: DMNO's Bitwarden plugin allows you to securely access your secrets stored in Bitwarden Secrets Manager. |
| 4 | +npmPackage: "@dmno/bitwarden-plugin" |
| 5 | +--- |
| 6 | + |
| 7 | +import { Steps, Icon } from '@astrojs/starlight/components'; |
| 8 | +import TabbedCode from '@/components/TabbedCode.astro'; |
| 9 | + |
| 10 | +This DMNO plugin allows you to securely access your secrets stored in [Bitwarden Secrets Manager](https://bitwarden.com/products/secrets-manager/). Please note that this plugin is **not compatible with Bitwarden's Password Manager product**. Authentication with Bitwarden uses [Machine Account Access Tokens](https://bitwarden.com/help/access-tokens/). |
| 11 | + |
| 12 | +## Installation & setup |
| 13 | + |
| 14 | +Install the package in the service(s) that will use secrets from Bitwarden. |
| 15 | + |
| 16 | +<TabbedCode packageName="@dmno/bitwarden-plugin" /> |
| 17 | + |
| 18 | +----- |
| 19 | + |
| 20 | +After installation, you'll need to initialize the plugin in your `config.mts` and add a config item to hold your machine account access token. You can explicitly wire the plugin up to the service account token if using multiple tokens at once, or it will be injected by default based on the `BitwardenSecretsManagerTypes.machineAccountAccessToken` type. It's ok if you have not created the machine account or access token - we'll do that in the next section. |
| 21 | + |
| 22 | +```diff lang="ts" title='.dmno/config.mts' |
| 23 | ++import { BitwardenSecretsManagerDmnoPlugin, BitwardenSecretsManagerTypes } from '@dmno/bitwarden-plugin'; |
| 24 | + |
| 25 | +// by default, access token will be injected using types |
| 26 | ++const bitwardenPlugin = new BitwardenSecretsManagerDmnoPlugin('bitwarden'); |
| 27 | + |
| 28 | +// or you can explicitly wire it up by path |
| 29 | ++const bitwardenPlugin2 = new BitwardenSecretsManagerDmnoPlugin('bitwarden', { |
| 30 | ++ accessToken: configPath('..', 'BWS_TOKEN') |
| 31 | ++}); |
| 32 | + |
| 33 | +export default defineDmnoService({ |
| 34 | + schema: { |
| 35 | ++ BWS_TOKEN: { |
| 36 | ++ extends: BitwardenSecretsManagerTypes.machineAccountAccessToken, |
| 37 | ++ // NOTE - the type itself is already marked as sensitive 🔐 |
| 38 | ++ }, |
| 39 | + }, |
| 40 | +}); |
| 41 | +``` |
| 42 | + |
| 43 | +:::tip[Plugin instance IDs] |
| 44 | +You must give each plugin instance a unique id so we can refer to it in other services and the [`dmno` CLI](/docs/reference/cli/plugin/). |
| 45 | + |
| 46 | +In this case we used `bitwarden`, but you can imagine splitting vaults and access, and having multiple plugin instances - for example `bitwarden/prod` for highly sensitive production secrets and `bitwarden/dev` for everything else. |
| 47 | +::: |
| 48 | + |
| 49 | +### Injecting the plugin in monorepo services |
| 50 | +In a monorepo, you are likely managing secrets for multiple services. If you will be using the same service account(s) to access those secrets, you can initialize a plugin instance once in your root service as seen above, and then inject it in child services. Note we must use that same id we set during initialization. |
| 51 | + |
| 52 | +```typescript title='apps/some-service/.dmno/config.mts' |
| 53 | +import { BitwardenSecretsManagerDmnoPlugin } from '@dmno/bitwarden-plugin'; |
| 54 | + |
| 55 | +// 💉 inject the already initialized plugin instead of re-initializing it |
| 56 | +const bitwardenPlugin = BitwardenSecretsManagerDmnoPlugin.injectInstance('bitwarden'); |
| 57 | +``` |
| 58 | + |
| 59 | + |
| 60 | +------------ |
| 61 | + |
| 62 | +## Setup Project & Secrets |
| 63 | + |
| 64 | +If you are already using Bitwarden Secrets Manager, you likely already have existing [projects](https://bitwarden.com/help/projects/) that contain [secrets](https://bitwarden.com/help/secrets/). If so, now would be a good time to review how they are all organized. If not, you should create at least one project, as each secret can have a parent project it belongs to, and access can be granted to projects rather than managing each secret individually. |
| 65 | + |
| 66 | +:::tip[Use projects to segment access] |
| 67 | +You should use multiple projects to segment your secrets following the [Principle of Least Privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege). This means at minimum, you should have one project for ultra-sensitive production secrets, and another for everything else. How much more you want to break things up is up to you and will depend on your security requirements. |
| 68 | + |
| 69 | +It is technically possible to create secrets without using projects and/or to assign permissions at the individual secret level, but we do not recommend it. |
| 70 | +::: |
| 71 | + |
| 72 | +## Setup Machine Account & Access Tokens |
| 73 | + |
| 74 | +Machine accounts can be granted access to projects, and each machine account can have multiple access tokens with optional expiration. How you want to manage this is up to you, but a sensible approach could be: |
| 75 | + |
| 76 | +- Production machine account has access to all projects |
| 77 | + - single access token is used at a time |
| 78 | +- Staging/CI machine account has access to all projects except ultra-sensitive prod secrets |
| 79 | + - each environment/CI/external tool could have a unique access token |
| 80 | +- Dev machine account has access to secrets needed for local dev |
| 81 | + - each developer could have a unique access token |
| 82 | + |
| 83 | +Expiring tokens are more secure, but require the overhead of rolling those tokens, which must be done manually. Not wanting to cause an outage, you may want to roll them manually without the pressure of a potentially forgotten expiry date. To roll without downtime, create a new token, redeploy, and then decommission the previous token. |
| 84 | + |
| 85 | +### |
| 86 | + |
| 87 | + |
| 88 | +Machine account access tokens now serve as your _secret-zero_ - which grants access to the rest of your sensitive config stored in Bitwarden. It must be set locally and in deployed environments, but it is sensitive so we must pass in the value as an _override_ rather than storing it within the config. Locally, this usually means storing it in your [`.env.local` file](/docs/guides/env-files/) and on a deployed environment you'll usually set it within some kind of UI, wherever you would normally pass in environment variables. |
| 89 | + |
| 90 | +```diff title=".dmno/.env.local" |
| 91 | ++BWS_TOKEN=0.abc123... |
| 92 | +``` |
| 93 | + |
| 94 | +Note that the config path of `BWS_TOKEN` is arbitrary and you can see how it was wired up from your config schema to the plugin input in the example above. |
| 95 | + |
| 96 | +------ |
| 97 | + |
| 98 | +## Add items to your schema |
| 99 | + |
| 100 | +With the plugin initialized and access wired up, now we must update our config schema to connect specific config values to data stored in Bitwarden secrets. |
| 101 | + |
| 102 | +Items are wired up using the secret UUIDs found in the Bitwarden UI. For example: |
| 103 | + |
| 104 | +```ts |
| 105 | +export default defineDmnoService({ |
| 106 | + schema: { |
| 107 | + ITEM_WITH_ID: { |
| 108 | + value: bitwardenPlugin.secretById('abc123-secretuuid-xyz789'), |
| 109 | + }, |
| 110 | + // example showing a switchBy and multiple plugin instances |
| 111 | + SWITCHED_ITEM: { |
| 112 | + value: switchBy('MY_ENV_FLAG', { |
| 113 | + _default: 'not-sensitive', |
| 114 | + staging: bitwardenDevSecrets.secretById('0123...'), |
| 115 | + production: bitwardenProdSecrets.secretById('789...'), |
| 116 | + }), |
| 117 | + }, |
| 118 | + }, |
| 119 | +}); |
| 120 | +``` |
| 121 | + |
| 122 | +## Caching |
| 123 | +In order to avoid rate limits and keep dev server restarts extremely fast, we heavily cache data fetched from external sources. After updating secrets in Bitwarden, if the item has been cached, you'll need to clear the cache to see it take effect. |
| 124 | + |
| 125 | +- Use the [`dmno clear-cache` command](/docs/reference/cli/clear-cache/) to clear the cache once |
| 126 | +- The [`dmno resolve`](/docs/reference/cli/resolve/) and [`dmno run`](/docs/reference/cli/run/) commands have cache related flags: |
| 127 | + - `--skip-cache` - skips caching logic altogether |
| 128 | + - `--clear-cache` - clears the cache once before continuing as normal |
| 129 | + |
| 130 | +:::tip[Active config iteration] |
| 131 | +While you are actively working on the config itself, `dmno resolve -w --skip-cache` will combine watch mode with skipping cache logic. |
| 132 | + |
| 133 | +Once you are satisfied, clear the cache once more and you are good to go. |
| 134 | +::: |
| 135 | + |
| 136 | + |
| 137 | +## Self-hosted |
| 138 | +In case you are self-hosting Bitwarden Secrets Manager, the `BitwardenSecretsManagerDmnoPlugin` also takes additional inputs for `apiServerUrl` and `identityServerUrl`. The values for this can be found in the Bitwarden UI under `Machine Accounts` > `Config`. See the [Bitwarden docs](https://bitwarden.com/help/machine-accounts/#configuration-information) for more details. |
| 139 | + |
| 140 | +```typescript |
| 141 | +const bitwardenPlugin = new BitwardenSecretsManagerDmnoPlugin('bitwarden', { |
| 142 | + apiServerUrl: 'https://vault.bitwarden.com/api', // default value |
| 143 | + identityServerUrl: 'https://vault.bitwarden.com/identity', // default value |
| 144 | +}); |
0 commit comments