bitwarden
Salt Extension Modules for Bitwarden
Introduction
This extension for Salt enables Salt to access and administer a Bitwarden vault/instance.
This project aims to eventually have 100% coverage of the Bitwarden Vault Management API and the Bitwarden Public (Organization Management) API. The Vault Management API is now covered for both reads and writes; the Public API is not yet implemented.
This extension is changing quickly, and while attempts will be made to not make breaking changes, the current focus is on functionality rather than syntax/API stability.
Requirements
This extension requires the bw Bitwarden CLI utility to be installed for functions that depend
on the Bitwarden Vault Management API. Installation instructions are available
here. It also has dependencies on the
pyhumps and requests python
packages, which are automatically installed.
Python 3.10 or later and Salt 3006 or later are required.
Talking to bw serve over a unix socket, which is the recommended deployment (see
Connecting to the API), requires Bitwarden CLI 2025.9.0 or later. Earlier
releases can only listen on a TCP port.
Installation
An example state file for installing and configuring this extension on EL8 (RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Scientific Linux, etc.):
# Install the NodeSource EL8 repo to get Node.js
cmd_nodesource-el8.repo:
cmd.run:
- name: curl -fsSL https://rpm.nodesource.com/setup_18.x | bash -
- creates:
- /etc/yum.repos.d/nodesource-el8.repo
# Bitwarden CLI depends on Node.js
pkg_nodejs:
pkg.installed:
- name: nodejs
- require:
- cmd_nodesource-el8.repo
# Install the Bitwarden CLI client (which includes the Vault Management REST API
# server)
npm_bitwarden_cli:
npm.installed:
- name: '@bitwarden/cli'
- require:
- pkg_nodejs
# Install the Bitwarden Salt Extension
pip_saltext.bitwarden:
pip.installed:
- name: saltext.bitwarden
- require:
- npm_bitwarden_cli
# Configure server options for Bitwarden REST API Server
file_/etc/sysconfig/bw-api:
file.managed:
- name: /etc/sysconfig/bw-api
- user: root
- group: root
- mode: "0644"
- contents: |
# Command-line options for bw serve
BITWARDENCLI_APPDATA_DIR=/etc/salt/.bitwarden
OPTIONS="--hostname unix:///run/bw-api/vault.sock"
- require:
- pip_saltext.bitwarden
- watch_in:
- service_bw-api
# Create a systemd service unit file since one is not included in the package.
# Reloads systemctl if the file changes.
file_/etc/systemd/system/bw-api.service:
file.managed:
- name: /etc/systemd/system/bw-api.service
- user: root
- group: root
- mode: "0644"
- contents: |
[Unit]
Description=Bitwarden Vault Management API
Documentation=https://bitwarden.com/help/cli/
After=network.target
[Service]
EnvironmentFile=-/etc/sysconfig/bw-api
User=salt
Group=salt
Type=simple
WorkingDirectory=~
# Creates /run/bw-api owned by salt:salt, mode 0750, and removes it on
# stop. Traversing that directory is what gates access to the socket,
# so only root and the salt group can reach the API.
RuntimeDirectory=bw-api
RuntimeDirectoryMode=0750
ExecStart=/bin/bw serve $OPTIONS
Restart=always
[Install]
WantedBy=multi-user.target
- require:
- file_/etc/sysconfig/bw-api
- watch_in:
- service_bw-api
# You must manually create files in /etc/salt/master.d/bitwarden.conf or
# /etc/salt/minion.d/bitwarden.conf (or both) depending on the context, with the following contents
# (substituting values as appropriate):
#
# bitwarden:
# driver: bitwarden
# cli_path: /bin/bw
# cli_conf_dir: /etc/salt/.bitwarden
# cli_runas: salt
# vault_url: https://bitwarden.com
# email: user@example.com
# password: CorrectHorseBatteryStaple
# vault_api_url: unix:///run/bw-api/vault.sock
# public_api_url: https://api.bitwarden.com
# client_id: 25fa6fc6-deeb-4b42-a279-5e680b51aa58
# client_secret: AofieD0oexiex1mie3eigi9oojooF3
# org_client_id: organization.d0e19db4-38aa-4284-be3d-e80cff306e6c
# org_client_secret: aWMk2MBf4NWXfaevrKyxa3uqNXYVQy
# Used for runner and SDB modules
file_/etc/salt/master.d/bitwarden.conf:
file.exists:
- name: /etc/salt/master.d/bitwarden.conf
- require:
- pip_saltext.bitwarden
# Used for execution, SDB and state modules
file_/etc/salt/minion.d/bitwarden.conf:
file.exists:
- name: /etc/salt/minion.d/bitwarden.conf
- require:
- pip_saltext.bitwarden
# Make sure the Bitwarden vault is logged in before we start the REST API server
# service otherwise the service will refuse to start
bitwarden_logged_in:
bitwarden.logged_in:
- name: logged_in
- use_cli: True
- profile: bitwarden
# Run the Bitwarden Vault Management REST API server `bw serve`
service_bw-api:
service.running:
- name: bw-api
- enable: True
- init_delay: 10
- require:
- bitwarden_logged_in
# State to reload systemctl
service_systemctl_reload:
module.run:
- name: service.systemctl_reload
- onchanges:
- file_/etc/systemd/system/bw-api.service
Nothing in that state restricts access to the API, because nothing needs to: the socket lives in a
directory only root and the salt group can traverse. On a TCP port the equivalent takes a set of
firewalld direct rules matching on --uid-owner; see
Connecting to the API.
The configuration files required vary depending on which module you wish to use:
| module type | file type |
|---|---|
| execution | minion |
| runner | master |
| sdb | master, minion |
| state | minion |
Connecting to the API
bw serve can listen on either a unix socket or a TCP port, and vault_api_url accepts both:
# Unix socket (recommended)
vault_api_url: unix:///run/bw-api/vault.sock
# TCP
vault_api_url: http://localhost:8087
Prefer the socket. The Vault Management API has no authentication of its own: once the vault is unlocked, whatever can reach the API can read every secret in it. On a TCP port that is every process on the host, and the traffic crosses the loopback interface in clear text. A socket is guarded by ordinary filesystem permissions, so access can be restricted to one user or group, and there is no port for anything else to find.
Access is gated by permission to traverse the socket's directory, so put the socket somewhere
restricted rather than relying on the mode of the socket file itself, which Node creates from the
process umask. Under systemd, RuntimeDirectory= and RuntimeDirectoryMode= do this for you, as in
the state above.
Socket paths are limited to about 108 bytes by the operating system, not by this extension or by
Bitwarden. A longer path fails at startup with Error: listen EINVAL: invalid argument, which does
not name the length as the cause. /run keeps paths comfortably short.
Do not pass --port alongside --hostname unix://.... CLI 2026.6.0 added DNS-rebinding
protection, which rejects any request whose Host header is not in an allowlist built from the
hostname and port bw serve was given. That check runs for unix sockets too, even though a browser
cannot reach one, and the only allowlist entries a socket client can match are the port-based ones.
The extension therefore sends Host: localhost:8087, matching the default. Passing --port moves
the allowlist and every request comes back 403.
If you are on a Bitwarden CLI older than 2025.9.0, or otherwise need TCP, bind it to localhost and
restrict it by owning UID. The Salt firewalld state module does not support direct rules, so the
configuration file has to be managed directly:
service_firewalld:
service.running:
- name: firewalld
- enable: True
- reload: True
file_/etc/firewalld/direct.xml:
file.managed:
- name: /etc/firewalld/direct.xml
- user: root
- group: root
- mode: "0644"
- contents: |
<?xml version="1.0" encoding="utf-8"?>
<!--
########################################################################
# #
# THIS FILE IS MANAGED BY SALT - DO NOT EDIT #
# #
# The contents of this file are managed by Salt. Any changes to this #
# file may be overwritten automatically and without warning. #
########################################################################
-->
<direct>
<rule ipv="ipv4" table="filter" chain="OUTPUT" priority="0">-o lo -p tcp --dport 8087 -m owner --uid-owner root -j ACCEPT</rule>
<rule ipv="ipv4" table="filter" chain="OUTPUT" priority="0">-o lo -p tcp --dport 8087 -m owner --uid-owner salt -j ACCEPT</rule>
<rule ipv="ipv4" table="filter" chain="OUTPUT" priority="1">-o lo -p tcp --dport 8087 -j REJECT</rule>
<rule ipv="ipv6" table="filter" chain="OUTPUT" priority="0">-o lo -p tcp --dport 8087 -m owner --uid-owner root -j ACCEPT</rule>
<rule ipv="ipv6" table="filter" chain="OUTPUT" priority="0">-o lo -p tcp --dport 8087 -m owner --uid-owner salt -j ACCEPT</rule>
<rule ipv="ipv6" table="filter" chain="OUTPUT" priority="1">-o lo -p tcp --dport 8087 -j REJECT</rule>
</direct>
- watch_in:
- service_firewalld
Both transports are exercised by the integration tests against a real bw serve.
Usage
The extension provides read and write access to a Bitwarden vault through execution, runner, state
and sdb modules. The execution and runner modules expose the same set of functions; the runner
runs them on the master, the execution module on a minion.
Reading and writing items
# Read
salt '*' bitwarden.get_item item_id=2fcd790a-70f7-43a2-b265-08a763873980
salt '*' bitwarden.get_password item_id=2fcd790a-70f7-43a2-b265-08a763873980
salt '*' bitwarden.list_items search="Example Item"
# Write
salt '*' bitwarden.create_folder name="Servers"
salt '*' bitwarden.create_item item='{"type": 1, "name": "Example", "login": {"username": "jdoe", "password": "hunter2"}}'
salt '*' bitwarden.delete_item item_id=2fcd790a-70f7-43a2-b265-08a763873980 permanent=True
Attachments, Sends and organization collections are managed the same way; see the module documentation for the full list.
States
Servers folder exists:
bitwarden.folder_present:
- name: Servers
Example login:
bitwarden.item_present:
- name: Example Login
- item:
type: 1
login:
username: jdoe
password: {{ salt['sdb.get']('sdb://bitwarden/by-uuid/.../password') }}
States support test=True. Secret values are redacted from the changes output by default,
because state returns are written to the job cache and the master's logs; set
show_secret_changes: True on an individual state if you need to see them.
Declaring secrets in SLS files puts them in your state tree, which is normally version controlled. Prefer rendering them from pillar or an
sdb://URI, or manage only non-secret attributes and generate passwords withbitwarden.generate_password.
Error handling
Failures raise exceptions rather than returning a value. salt.exceptions.SaltInvocationError
means the arguments were wrong; subclasses of salt.exceptions.CommandExecutionError
(BitwardenNotFoundError, BitwardenAuthenticationError, BitwardenUnavailableError,
BitwardenServerError) mean the operation failed. Salt reports these itself, so there is no need
to test return values for falsiness.
A note on consistency
bw serve answers from a local cache that it populates asynchronously. An object that was just
created is not reliably visible to the very next request, and a read issued immediately after a
write can return the pre-write value.
This extension retries once after a sync when an object is unexpectedly missing, and the state
modules sync before comparing so they never diff against stale data. If you are scripting your own
sequence of calls against the API, call bitwarden.sync between a write and a dependent read.
SDB
# SDB via runner module
salt-run sdb.get 'sdb://bitwarden/by-uuid/2fcd790a-70f7-43a2-b265-08a763873980/password'
CorrectHorseBatteryStaple
# SDB via execution module
salt-call sdb.get 'sdb://bitwarden/by-uuid/2fcd790a-70f7-43a2-b265-08a763873980/password'
local:
CorrectHorseBatteryStaple
As always, you can also reference SDB modules in your pillar files:
example_pillar:
some_password: {{ salt['sdb.get']('sdb://bitwarden/by-uuid/2fcd790a-70f7-43a2-b265-08a763873980/password') }}
The format of the SDB URI is as follows:
sdb://<profile>/by-uuid/<uuid>/<object>
Where <profile> is the profile defined in the master or minion configuration
file, <uuid> is the UUID of the item, and <object> is one of:
- name
- username
- password
- totp
- notes
- creation_date
- revision_date
- deleted_date
- password_revision_date
Additionally, password history can be retrieved with the following SDB URI format:
sdb://<profile>/by-uuid/<uuid>/password_history/by-index/<index>
Where <profile> is the profile defined in the master or minion configuration
file, <uuid> is the UUID of the item, and index is a non-negative
integer specifying which password to retrieve from the history (0 being the
current password, 1 being the previous password, and so forth).
Lastly, custom fields can be retrieved with the following SDB URI format:
sdb://<profile>/by-uuid/<uuid>/fields/by-name/<field_name>/<object>
Where <profile> is the profile defined in the master or minion configuration
file, <uuid> is the UUID of the item, <field_name> is the name of
custom field, and object is one of:
- value
- type
- linked_id
Note that the custom field name must be unique within an item. Bitwarden does not enforce unique custom field names, so that is left up to the user.
sdb.set writes to an item's name, username, password, notes or a custom field's value:
salt-run sdb.set 'sdb://bitwarden/by-uuid/2fcd790a-70f7-43a2-b265-08a763873980/password' 'NewCorrectHorseBatteryStaple'
It only sets values within an existing item; use the execution, runner or state modules to create items.
The UUID of an item can be found using the Bitwarden CLI:
bw list items --search "Google Account" --pretty
[
{
"object": "item",
"id": "2fa63ad5-e4e4-43d4-a089-3fadcf455be2",
"organizationId": null,
"folderId": null,
"type": 1,
"reprompt": 0,
"name": "Google Account",
"notes": null,
"favorite": false,
"login": {
"uris": [
{
"match": null,
"uri": "https://accounts.google.com"
}
],
"username": "user@example.com",
"password": "aTjSsJvhQY5E24",
"totp": "AEM1HEESIEV8YAED8THUBEHOOW",
"passwordRevisionDate": null
},
"collectionIds": [],
"revisionDate": "1970-01-01T00:00:00.000Z"
}
]
Development
python -m venv .venv && . .venv/bin/activate
pip install -e '.[tests,dev,lint]'
pre-commit install
# Unit tests only -- no vault, no container runtime needed
pytest tests/unit/
# Everything, including integration tests
pytest tests/
The integration tests bring up a Vaultwarden
container, register an account against it and run a real bw serve against that account, so they
exercise the actual API rather than a mock. They need a container runtime and the bw CLI, and
skip themselves cleanly when either is missing.
Vaultwarden is served over TLS with a throwaway self-signed certificate, because current Bitwarden CLI releases refuse to talk to a plain-HTTP server.
Under rootless Podman, point the Docker SDK at Podman's socket:
systemctl --user start podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
pytest tests/
Vaultwarden implements the personal vault faithfully but only part of the organization surface, so organization collections, members and confirmation are covered by unit tests only. Exercising those for real needs a paid Bitwarden organization.
Releasing
Releases are published to PyPI by GitLab CI using PyPI Trusted Publishing, so no API token is stored in the project. GitLab mints a short-lived OIDC token, twine exchanges it for a PyPI token valid for about 15 minutes, and uploads with that.
One-time setup on PyPI, under the project's Publishing settings, add a GitLab publisher:
| Field | Value |
|---|---|
| Namespace | ggiesen |
| Repository name | saltext-bitwarden |
| Pipeline filepath | .gitlab-ci.yml |
| Environment | release |
The environment is optional to PyPI but recommended: pair it with a protected environment in GitLab to require manual approval before an upload runs.
To cut a release:
# 1. Add changelog fragments as you go, under changelog/
# named <issue>.<type>.md, or +<slug>.<type>.md when there is no issue.
# 2. Fold them into CHANGELOG.md
towncrier build --version X.Y.Z --yes
# 3. Commit, tag, push. Tags are v-prefixed; setuptools_scm strips the v, so
# the released version is still X.Y.Z.
git commit -am "Release X.Y.Z"
git tag -a vX.Y.Z -m "Release X.Y.Z"
git push && git push origin vX.Y.Z
Pushing the tag runs the pipeline. The publish job only appears for tags
matching vX.Y, and it is manual: nothing reaches PyPI until someone runs
it, so tagging a release does not auto-fire an upload. It refuses to upload if
the built version does not match the tag, which catches a shallow clone or a
dirty tree producing a .dev version.
Docs
Module documentation is available at https://ggiesen.gitlab.io/saltext-bitwarden.
Bugs
Bugs can be reported using the Issue Tracker.
Contributing
All contributions are welcome and very much appreciated. Contributing guide coming soon. Contributing can take many forms, including:
- Reporting bugs
- Feature requests
- Code submissions (bug fixes/new features/improve code quality)
- Writing tests
- Writing documentation
- Detailing use cases
- Writing blog posts
License
This project is licensed under the Mozilla Public License 2.0. See LICENSE for the licence text.
Releases up to and including 0.1.0 were published under the Apache Software
License. That grant is not retracted: anyone who obtained those releases keeps
their rights under Apache 2.0. MPL 2.0 applies from the next release onwards.
Some of the packaging and tooling in this repository (noxfile.py,
docs/conf.py, the .pre-commit-hooks/ scripts and parts of the test
scaffolding) originates from the Apache-2.0 licensed
salt-extension-copier
template. MPL 2.0 is a file-level licence and explicitly allows combining with
other terms, so those files may still carry their original licence.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file saltext_bitwarden-0.2.0.tar.gz.
File metadata
- Download URL: saltext_bitwarden-0.2.0.tar.gz
- Upload date:
- Size: 111.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9878f0ea648ac24fd06fb8018cf17c9a2b49369fb0441b07ec8a51082cff68dd
|
|
| MD5 |
d43d46d126547d3521689492bb713bed
|
|
| BLAKE2b-256 |
89f34bcf84cb1fab76fd091ca4ea3e00bcdb31e1375a457e263775070226013a
|
File details
Details for the file saltext_bitwarden-0.2.0-py2.py3-none-any.whl.
File metadata
- Download URL: saltext_bitwarden-0.2.0-py2.py3-none-any.whl
- Upload date:
- Size: 51.6 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db2660ed9a32a728752d5b47947490af0f6804c8c4bb5b20f7b0f0f886e5e7bc
|
|
| MD5 |
a827e71bdffd7d7ead82771d6aa1a1a4
|
|
| BLAKE2b-256 |
09f92e0bbe25427f0d9e2f96c180b33da51e6f49ce5b825fadfb563b05dd66d4
|