Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bwsec

License: MIT Crates.io

bwsec syncs secrets between a Bitwarden/Vaultwarden vault and local files.

It manages a bw serve daemon, reads a declarative bwsec.toml config, fetches vault items and attachments in parallel, and writes files to disk with strict path containment and 0600 permissions.

Features

  • Full round-trip: scaffold with init, upload with push, fetch with pull, audit with diff, wipe with clean
  • Single binary, zero runtime deps — no jq, base64, or extra scripts required
  • Managed bw serve daemon — no manual bw session handling needed
  • Parallel attachment downloads, copy_to for writing one secret to multiple paths
  • Conflict-safe push — name-keyed entries always create a new vault item (never overwrite an existing one) and are auto-pinned to the vault UUID; UUID-keyed entries update the matched item by ID
  • Non-interactive auth for CI--password-file, --password-env, or $BW_PASSWORD
  • Multi-account--bw-config / $BWSEC_BW_CONFIG sets $BITWARDENCLI_APPDATA_DIR
  • Pre-push backups — old attachments saved to .bwsec-backups/<timestamp>/ before replacement
  • Strict path containment — relative paths only, no .., destinations verified under project root
  • Secure file permissions — secrets written to disk are created at 0600 (owner read/write only) on Unix

When to use this

If your team already keeps project secrets as Bitwarden or Vaultwarden vault attachments and pulls them with ad-hoc bw/jq/base64 shell scripts, bwsec replaces that with a declarative config and a single binary.

bwsec is a thin wrapper around the bw CLI — its capability ceiling is whatever bw exposes. If you are not already using the vault as a file store, a purpose-built alternative may be a better fit:

  • Bitwarden Secrets Manager — machine-account-based secrets platform with its own CLI; models secrets as key-value pairs, not files.
  • SOPS + age — encrypted secrets committed to git; no external daemon or vault account required.

Prerequisites

  • Bitwarden CLI (bw) installed and on $PATH
  • A Bitwarden or Vaultwarden account with your project secrets as vault items

Installation

Homebrew (macOS and Linux)

brew install pisand/tap/bwsec

Cargo

cargo install bwsec

Pre-built binary

Download the latest archive for your platform from the GitHub Releases page, extract, and place the binary on your $PATH. Builds are provided for macOS (Apple Silicon + Intel), Linux (x86-64 + ARM64, musl), and Windows (x86-64).

Build from source

git clone https://github.com/pisand/bwsec.git
cd bwsec
cargo build --release
# binary: target/release/bwsec

Quick start

Option A — interactive scaffold (recommended for new projects)

bwsec init   # scaffold bwsec.toml, review in $EDITOR
bwsec push   # upload local files as vault attachments

Option B — manual config + pull

Create a bwsec.toml in your project root and run bwsec:

collection_id = "5cd3af3c-0afe-415d-963b-3ce5a783af48"

[items]
[items.".env"]
path = ".env"

After running bwsec push, the name key ".env" is automatically rewritten to the vault item's UUID in bwsec.toml.

See config.example.toml for the full configuration reference.

Commands

Run bwsec <command> --help for the full flag list. All commands accept --bw-config <DIR> and --bw-port <PORT> as global flags.

Command Description
pull (default) Fetch secrets from the vault and write them to local files
diff Compare vault content against local files; exits 1 if any differ or are missing
init Interactively scaffold bwsec.toml and open it in $EDITOR (TTY required)
push Upload local files to the vault as attachments; backs up old ones first (TTY required)
clean Delete all secrets written locally by bwsec (no vault access needed)

Common flags: -n/--dry-run, -v/--verbose, -q/--quiet, --no-sync, -j/--max-parallel (default: 8), --password-env, --password-file.

Configuration

bwsec.toml lives in the project root. All item paths are resolved relative to the directory containing the config file.

# Optional: restrict fetch to a single collection (faster, narrows scope)
# Must be a valid UUID (36 characters, 4 dashes)
collection_id = "5cd3af3c-0afe-415d-963b-3ce5a783af48"

# Optional: skip vault sync before operations (default: sync = true)
# sync = false

# Optional: Bitwarden CLI data directory for multi-account setups
# bw_config = "~/.config/bw-work"

[items]

# Name key — on `push`, always creates a new vault item and rewrites this
# key to the vault UUID automatically. On `pull`/`diff`, matches by name.
[items.".env"]
path = ".env"

# copy_to: also write to a second path after fetching
[items."sentry.properties"]
path = "android/sentry.properties"
copy_to = "ios/sentry.properties"

# UUID key — set automatically after the first push. On `push`, updates the
# existing item by ID. On `pull`/`diff`, also matches by ID.
[items."abc12345-0afe-415d-963b-3ce5a783af48"]
path = "special.env"

See config.example.toml for a complete reference including all fields and defaults.

Authentication & non-interactive use

bwsec resolves the master password in this order:

  1. --password-file <FILE> — first non-empty line of the file
  2. --password-env <VAR> — value of the named environment variable
  3. $BW_PASSWORD — detected automatically
  4. Interactive prompt — requires a TTY; fails in non-interactive environments

For CI, combine --password-env with --yes (skip confirmation) to run fully non-interactively:

bwsec pull --password-env VAULT_PASS --yes --no-sync --quiet

For multi-account setups, point bwsec at a specific Bitwarden CLI data directory via --bw-config, $BWSEC_BW_CONFIG, or the bw_config config field — all three set $BITWARDENCLI_APPDATA_DIR. For parallel CI jobs on the same host, use --bw-port to give each job an isolated daemon port.

How it works

  1. Walks up from cwd to find bwsec.toml. The config's parent directory becomes the project root.
  2. Spawns a bw serve daemon and unlocks it with the resolved master password.
  3. Optionally syncs the vault, then fetches all items in one request (scoped to collection_id if set).
  4. pull/diff: matches each config item by UUID (exact) or by name (case-sensitive). push: UUID-keyed entries update the matched item by ID; name-keyed entries always create a new vault item, then rewrite the config key to the returned UUID. Downloads attachments in parallel.
  5. Verifies every destination path stays within the project root, then writes secret files at mode 0600 on Unix.
  6. Terminates the daemon on exit.

Security

bwsec delegates all crypto and API access to the bw CLI — it makes no direct network requests, uses no unsafe Rust, and links against no crypto or networking libraries. bwsec.toml contains only item names and local paths, never secret values.

Property How
File permissions Secrets written by pull and push are created at 0600 on Unix; backup copies in .bwsec-backups/ are also written at 0600 and the backup directory is created at 0700
No secret logging Only item names and paths are printed, never content
Path containment Paths must be relative and resolve within the project root; .. rejected
Symlink protection Destination paths canonicalized and verified immediately before each write
Disk flush All writes call sync_all()

Known limitations: file permissions are not enforced on Windows (including backup copies in .bwsec-backups/); secret content is not zeroed from process memory; BW_SESSION is passed as an environment variable (standard bw behavior); a narrow TOCTOU window exists between the path-containment check and the file open.

Behavioral notes

  • First attachment only. When a vault item has multiple attachments, only the first is fetched; any extras are silently ignored.
  • push never matches existing vault items by name. Name-keyed config entries (e.g. [items.".env"]) always create a new vault item — they never update an item that happens to share the same name. This prevents accidental overwrites when multiple projects use the same secret name. After creation, the config key is rewritten to the vault UUID.
  • push attachment replacement matches by filename. For UUID-keyed items, if the existing vault item already has an attachment with the same filename, the old one is backed up and replaced. A different filename uploads as a new attachment alongside existing ones. Renaming a local file before push therefore uploads a new attachment; remove the stale one from the vault manually.
  • push rewrites bwsec.toml in place. Two edits can occur: selecting a collection interactively writes collection_id, and creating new vault items rewrites the name key to the vault UUID. Both are non-atomic string edits; complex custom comments near the affected lines may be affected.
  • init uses the file basename as the item key. Selecting two files with the same name (e.g. foo/.env and bar/.env) generates a key collision in the draft TOML. Fix the duplicate keys in $EDITOR before continuing.
  • clean prompts before deleting .bwsec-backups/. After removing the paths listed in bwsec.toml, clean asks whether to also delete the local backup directory. Answer n (or press Ctrl-C) to keep backups; in quiet or non-interactive mode the prompt is skipped and backups are left intact. Add .bwsec-backups/ to your .gitignore to prevent accidentally committing plaintext secrets:
    echo '.bwsec-backups/' >> .gitignore
    

Report security issues privately via GitHub Security Advisories.

Troubleshooting

Problem Solution
bw not found Install the Bitwarden CLI and ensure it's on $PATH
"Not logged in" Run bw login first
Vault locked with no TTY Use --password-env or --password-file
Items not found pull/diff: check names are exact and case-correct, or use a UUID key. push: name keys always create new items; use UUID keys to update an existing item by ID
Slow fetch Set collection_id to limit the query scope
Port conflict between CI jobs Use --bw-port to assign each job a unique port
Command hangs indefinitely Kill the process; run pkill -f 'bw serve' to clear any stuck daemon; retry
Plaintext files in .bwsec-backups/ Run bwsec clean and answer y at the backup prompt, or delete the directory manually; add .bwsec-backups/ to .gitignore

Contributing

Issues and pull requests are welcome on GitHub.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages