vaultwarden-crs-plugin

A drop-in OWASP CRS 4.0+ plugin that makes the Core Rule Set play nicely with Vaultwarden — the Rust, Bitwarden-compatible server — and optionally locks the host down to Vaultwarden’s known route map.

GitHub  ·  Self-hosted Vaultwarden guide  ·  All CRS plugins

⭐ View on GitHub →

LintIntegration testsApache + ModSecurity v2nginx + libmodsecurity3nginx + CorazaSecurity Corpus

Why Vaultwarden needs a different approach

Vaultwarden is a JSON API: the web vault, browser extensions, mobile, desktop and the Bitwarden CLI all POST application/json bodies whose argument names are JSON keys that vary per endpoint and per client version. A vimbadmin-style ARGS_NAMES allowlist would false-block real clients, so this plugin deliberately ships no arg-name allowlist for JSON bodies. Its positive-security layer is a path allowlist only. Bodies are end-to-end encrypted (EncString 2.<iv>|<ct>|<mac> blobs + base64), so the before-rules strip the known-noisy base64/SQLi/PHP target families on the encrypted write paths rather than weakening the whole engine.

What it does

  • False-positive exclusions (vaultwarden-before.conf) — surgical, host-scoped exclusions so legitimate inputs don’t trip CRS: the Argon2 admin token, the OAuth password-grant hashes on /identity/connect/token, the EncString cipher/account/send blobs under /api, and user-supplied icon domains.
  • Positive security / path allowlist (vaultwarden-after.conf, opt-in) — allow Vaultwarden’s real mount points (/api, /identity, /admin, /events, /icons, /notifications, /attachments, static routes and the web-vault tree), deny everything else. Stops /.env / /wp-login.php scanner noise before it reaches the backend. The route map is derived from Vaultwarden’s source (src/main.rs mount points + src/api/web.rs static routes) and the bundled web-vault static tree extracted from the official Docker image — not guessed.

Additional hardening

The positive-security layer also enforces an HTTP-method allowlist (only GET/POST/PUT/DELETE/HEAD/OPTIONS; TRACE/CONNECT/PATCH/junk verbs denied), requires application/json on /api writes (except multipart sends and attachment uploads), anchors static-file paths to a single path segment so a deep fake path like /x/y/z.json no longer slips through, and feeds the CRS inbound anomaly score on every block so fail2ban / CRS DoS layers see the probe.

What’s new in 2.0.0

Version 2.0.0 is a full re-verification of the route map against fresh Vaultwarden source and the extracted web-vault Docker tree, plus fixes to four issues found in a dual (independent + self) security audit:

  • Trailing-slash allowlist bypass — fixed. In earlier releases the root branch matched the trailing slash of any path, so /wp-login.php/, /.env/ and /phpmyadmin/ sailed straight past the path allowlist. The branch is now anchored.
  • Enforcement rules could disable themselves — fixed. The method / Content-Type / path deny rules were tagged OWASP_CRS, so the auth-path false-positive strip (ctl:ruleRemoveByTag=OWASP_CRS) — or any host-level OWASP_CRS tag strip — silently switched the positive-security layer off on those paths. The tag was removed from the enforcement rules.
  • Deep-nested probe tightening. The mount/static-dir branch matched its token at any depth, so /wp-content/images/shell.php slipped through on the images segment. It now allows at most one optional basepath segment.
  • Case-insensitive Content-Type. The application/json check now folds case, so Content-Type: Application/JSON no longer draws a false 415.

The fresh scan also corrected the route map: root-level hashed JSON/XML assets are now allowed (a hashed web-vault bundle was being 404’d), the stale root-level admin-bundle branch was dropped (those files are served under /vw_static/, not the webroot), and the token form-field allowlist gained the send-access fields send_id and password_hash_b64. Every fix is proven by a Coraza runtime test that replays the exact bypass and confirms the deny fires.

Optional arg-name allowlist (experimental)

JSON bodies are never name-allowlisted, but the two non-JSON surfaces are stable and fully enumerable, so the plugin adds an optional, separately-gated allowlist for them: the /identity/connect/token form fields (fixed by the ConnectData struct) and GET query-string parameter names. These live in before.conf so they evaluate before CRS’s anomaly-blocking rule. Enable only after a DetectionOnly burn-in.

Install & enable

Copy the three files (vaultwarden-config.conf, vaultwarden-before.conf, vaultwarden-after.conf) into your CRS plugins/ directory. The plugin is OFF by default — it weakens CRS on Vaultwarden’s routes, so enable it per vhost only, never globally:

server {
    server_name vault.example.com;
    modsecurity on;
    modsecurity_rules '
        SecAction "id:9530001,phase:1,nolog,pass,setvar:tx.vaultwarden-plugin_enabled=1"
    ';
}

On Apache/mod_security2, set the same variable in the matching <Location>/<VirtualHost>. Roll the path allowlist out in CRS DetectionOnly first, then flip to blocking. Requires CRS 4.0+ on any ModSecurity-compatible WAF.