Skip to main content

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.

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 localhost --port 8087"
    - 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=~
        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: http://localhost:8087
#    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

# Use firewalld to protect the Bitwarden Vault Management REST API server
service_firewalld:
  service.running:
    - name: firewalld
    - enable: True
    - reload: True

# Lock down access to the Bitwarden Vault Management REST API to only the root and salt users using
# firewalld because once the vault is unlocked, access is completely unauthenticated.
#
# See https://github.com/bitwarden/clients/issues/3932
#
# We directly manage the direct rules configuration file because the Salt firewalld state module
# doesn't support direct rules.
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

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

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 with bitwarden.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 Apache Software License. See LICENSE for the licence text.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

saltext_bitwarden-0.1.0.tar.gz (99.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

saltext_bitwarden-0.1.0-py2.py3-none-any.whl (45.2 kB view details)

Uploaded Python 2Python 3

File details

Details for the file saltext_bitwarden-0.1.0.tar.gz.

File metadata

  • Download URL: saltext_bitwarden-0.1.0.tar.gz
  • Upload date:
  • Size: 99.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for saltext_bitwarden-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bcebe2968da1c28f3b7aed49b23212503764eb266d94463b7be87b75a3d6318c
MD5 7abf23267471ea8c31f3c28d6e0d4aa4
BLAKE2b-256 c6512349c4238e7317b7c3ebaf409365a1cac864ebe9cb563934f780c7341cfe

See more details on using hashes here.

File details

Details for the file saltext_bitwarden-0.1.0-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for saltext_bitwarden-0.1.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 8b00bcd831018d3070a742b3ea9566f5290ed708f3cc658f598b7f13cadd97ad
MD5 6ca677f89c9183e3852954fc8a5b1ba9
BLAKE2b-256 df62504221bb6e3580b5b64a617c0769ee6eb2dc826c993e7e27a46e5667a660

See more details on using hashes here.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page