Backup and sync helpers for Bitwarden/Vaultwarden, built on top of
rbw (specifically
pschmitt/rbw, a fork with multi-account
support, native vault-to-vault mirroring, and non-interactive
login/unlock/export/import/purge). rbw does the heavy lifting (auth,
export/import, attachments, org/collection management); these scripts are
thin wrappers adding backup rotation, healthchecks, and idempotent org/
collection creation on top.
The backup archive format changed from the old bw-CLI-based version of
this repo: it's now whatever rbw export produces (its own JSON, optionally
gpg-encrypted), not the old bw export + bw list items + attachments
tarball. There's no compatibility shim -- old archives from before this
rewrite must be restored with the old bw-CLI-based tooling.
Every account used here (for backup, or as sync source/destination) must
already be configured in rbw's config.json (name/email/baseUrl) --
these scripts only ever supply the master password (and, once, a personal
API key for rbw register) at runtime. The NixOS module (see below)
renders config.json for you; for plain Docker usage, entrypoint.sh
renders it from env vars on every start.
For interactive Android/Termux use, install the
pschmitt/rbw fork rather than upstream
rbw. It includes native termux-keystore unlock
support; configure accounts.<name>.unlock.termux in rbw's
config.json after running the one-step rbw termux enroll flow. The
enrollment alias defaults to rbw-<account>; set termux_key_alias in
config.json or RBW_TERMUX_KEY_ALIAS to reuse a different Keystore key.
The container jobs documented here continue
to receive their password through their existing environment-file mechanism.
Note on bitwarden.com: the official server requires a one-time rbw register (personal API key) per account before scripted login works (bot
detection). Both scripts run this automatically before login, but it's a
no-op unless you supply that account's *_REGISTER_CLIENT_ID/
*_REGISTER_CLIENT_SECRET env vars (see below) -- get the key from
bitwarden.com.
podman run -it --rm \
-v /tmp/data:/data \
-e ACCOUNT_EMAIL=me@example.com \
-e BW_PASSWORD=xxxx \
-e ENCRYPTION_PASSPHRASE=mySecret1234 \
-e BW_BACKUP_RETENTION=30 \
-e CRON="0 23 * * *" \
ghcr.io/pschmitt/rbw-auto:latestACCOUNT(optional, default:backup): the rbw account name.ACCOUNT_EMAIL: the account's email address (used to renderconfig.json).ACCOUNT_BASE_URL(optional): the account's server URL, omit for the official bitwarden.com.BW_PASSWORD: the account's master password.BW_TOTP_SECRET(optional): the account's TOTP secret (base32, the same one an authenticator app would use), if it has TOTP-based 2FA enabled. A fresh code is generated per login/unlock viaoathtool.BW_BACKUP_REGISTER_CLIENT_ID/BW_BACKUP_REGISTER_CLIENT_SECRET(optional): personal API key, used once to runrbw registernon-interactively against bitwarden.com.BW_BACKUP_ATTACHMENTS(optional, default:1): set to0to skip downloading/embedding attachment contents (faster, smaller, but attachments won't be restorable from that backup).ENCRYPTION_PASSPHRASE(optional): if set, backups are gpg-encrypted (rbw export --encrypt) with this passphrase.BW_BACKUP_RETENTION(optional, default:30): how many backups to keep. Set to0to disable rotation.KEEPis deprecated; use this instead.CRON(optional): if set, runs the backup periodically on this schedule instead of once.HEALTHCHECK_URL(optional): pings Healthchecks.io (or a compatible endpoint) when the backup starts, completes successfully, or fails.BW_BACKUP_DIR(optional, default:/data): where backups are written. Mount your volume accordingly.
Run the container with the sync command to mirror one Bitwarden/
Vaultwarden account's vault into another (via rbw mirror -- entries and
attachments, no temp files):
podman run -it --rm \
-e SRC_ACCOUNT_EMAIL=you@example.com \
-e SRC_BW_PASSWORD=xxxx \
-e DEST_ACCOUNT_EMAIL=you@vaultwarden.example \
-e DEST_ACCOUNT_BASE_URL=https://vault.example.com \
-e DEST_BW_PASSWORD=xxxx \
ghcr.io/pschmitt/rbw-auto:latest syncSRC_ACCOUNT/DEST_ACCOUNT(optional, default:source/destination): the rbw account names.SRC_ACCOUNT_EMAIL/DEST_ACCOUNT_EMAIL,SRC_ACCOUNT_BASE_URL/DEST_ACCOUNT_BASE_URL: connection metadata forconfig.json.SRC_BW_PASSWORD/DEST_BW_PASSWORD: the two accounts' master passwords.SRC_BW_TOTP_SECRET/DEST_BW_TOTP_SECRET(optional): TOTP secrets for whichever account(s) have TOTP-based 2FA enabled, same asBW_TOTP_SECRETabove.SRC_REGISTER_CLIENT_ID/SRC_REGISTER_CLIENT_SECRET,DEST_REGISTER_CLIENT_ID/DEST_REGISTER_CLIENT_SECRET(optional): personal API keys forrbw register, same as above.BW_SYNC_MODE(optional, default:personal):personal: mirror the entire source vault into the destination account's personal vault, 1:1.collections: mirror into one or more destination organization collections (see below). Each configured name is handled based on whether the source account has a same-named collection: if so, that destination collection gets a scoped 1:1 mirror of just that source collection; if not, it gets a full mirror of the entire source vault instead (e.g. useful for a "whole vault" collection with no source-side counterpart).
DEST_BW_PURGE_VAULT(optional,personalmode only): if set to1, wipes the destination's personal vault before importing (server-side purge, same asrbw purge-vault). Entries in an org collection are never touched by this.DEST_BW_ORG/DEST_BW_COLLECTIONS(required incollectionsmode): the destination organization name and a comma-separated list of collection names to mirror into, e.g.DEST_BW_COLLECTIONS="default,Some Other Collection". The org and any missing collections are created automatically. The source account is only ever read, never modified.BW_SYNC_ATTACHMENTS(optional, default:1): set to0to skip attachments.BW_SYNC_OVERWRITE(optional, default:1): set to0to leave existing destination entries untouched instead of overwriting them.HEALTHCHECK_URLworks here too; sync pings start/fail/success.
The native rbw mirror --config FILE interface can also execute several
mirror specs sequentially from one YAML or JSON file. This is what the NixOS
module uses; the file is declaratively rendered and contains no credentials.
See the rbw documentation for the complete schema.
flake.nix exports nixosModules.default, providing services.rbw-auto.
Backup jobs remain independent named service/timer pairs. Enabled sync jobs
are instead rendered into one declarative rbw mirror --config plan and run
sequentially by the single rbw-auto-sync service/timer, avoiding startup
races between jobs sharing an rbw account. Sync jobs must use the same
period; their individual workDir markers and Monit checks remain
independent. Both job kinds share one system user/group and one
declaratively-rendered rbw config.json listing their accounts. Every
oneshot has an ExecStopPost backstop that terminates its rbw agents.
Timer jitter is controlled with services.rbw-auto.backupRandomizedDelaySec
and services.rbw-auto.syncRandomizedDelaySec; both default to "1h" and
map directly to systemd's RandomizedDelaySec.
services.rbw-auto.backupJobs.personal = {
account = {
name = "personal";
email = "me@example.com";
};
environmentFiles = [ config.sops.secrets."rbw-auto-backup-personal".path ];
};
services.rbw-auto.syncJobs.personal = {
sourceAccount = {
name = "personal";
email = "me@example.com";
};
destAccount = {
name = "vaultwarden";
email = "me@example.com";
baseUrl = "https://vault.example.com";
};
purgeDestination = true;
environmentFiles = [ config.sops.secrets."rbw-auto-sync-personal".path ];
};
services.rbw-auto.syncJobs.collections = {
sourceAccount = {
name = "personal";
email = "me@example.com";
};
destAccount = {
name = "vaultwarden";
email = "me@example.com";
baseUrl = "https://vault.example.com";
};
mode = "collections";
collections = {
org = "Example-Org";
names = [ "Shared" ];
};
environmentFiles = [ config.sops.secrets."rbw-auto-sync-org-collections".path ];
};See nix/module.nix for the full option list.
Use the rbw fork from pschmitt/rbw for
inspection and conversion. It understands the archive format produced by
rbw-auto directly, so there is normally no need to manually invoke gpg
or unpack the tarball. Set the same passphrase used by ENCRYPTION_PASSPHRASE
in RBW_EXPORT_PASSPHRASE:
export RBW_EXPORT_PASSPHRASE='mySecret1234'
rbw list --from-file data/bw-export-xxx.tar.gz.gpg
rbw get --from-file data/bw-export-xxx.tar.gz.gpg github
rbw show --from-file data/bw-export-xxx.tar.gz.gpg githubFor an interactive browser, use the TUI. Add --write to edit the exported
file; write operations create a .bak next to it before changing anything:
rbw tui --from-file data/bw-export-xxx.tar.gz.gpg
rbw tui --from-file data/bw-export-xxx.tar.gz.gpg --writeThe other file-backed commands include list, search, code, history,
add, edit, set, remove/delete, archive, unarchive, restore,
and attachment list/get/create/rm. For example:
rbw attachment list --from-file data/bw-export-xxx.tar.gz.gpg github
rbw export --from-file data/bw-export-xxx.tar.gz.gpg \
--format bitwarden-json --output vault.json
rbw export --from-file data/bw-export-xxx.tar.gz.gpg \
--format bitwarden-csv --output vault.csvIf putting the passphrase in the environment is not suitable, every
--from-file command accepts --passphrase PASSPHRASE; this may be exposed
through ps and shell history. The older
--from-file-passphrase PASSPHRASE spelling remains an alias. --attachments
must have been used when the backup was created if attachment contents should
be available to the TUI, attachment commands, or zip conversion. The legacy
decrypt.sh wrapper remains available, but the native rbw
commands preserve the export format and support direct querying/conversion.