From 8c90d6809bf0779389b3e830a93fb3546e38987e Mon Sep 17 00:00:00 2001 From: TSYS Group COO Date: Thu, 13 Aug 2026 13:00:53 -0500 Subject: [PATCH] feat: replace Node.js bw CLI with native Rust binary + fix module name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace npm-based @bitwarden/cli with the pre-compiled native Rust bw binary (v2026.7.0) to eliminate Node.js from the credential management layer for CMMC/ITAR/STIG audit readiness. Changes: - Dockerfile: download native bw binary instead of npm install; add python3-pip for Playwright dependencies - bw_helper.py: renamed from bw-helper.py (Python can't import hyphens); added BW_SERVER config for self-hosted instance; use --passwordfile for unlock (more reliable with native binary); removed TOTP from login flow (API key auth does not require it) - provision-agent.py: pass BW_SERVER env var to BitwardenHelper - docker-compose.yml: add BW_SERVER env var - .env.example: add BW_SERVER, document TOTP as optional Verified: dry-run passes, bw status/auth/generate all work inside the provisioner container against pwvault.turnsys.com. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- .env.example | 6 ++++- Dockerfile | 14 ++++++++--- bw-helper.py => bw_helper.py | 49 ++++++++++++++++++++---------------- docker-compose.yml | 1 + provision-agent.py | 1 + 5 files changed, 45 insertions(+), 26 deletions(-) rename bw-helper.py => bw_helper.py (82%) diff --git a/.env.example b/.env.example index e9ddc6e..8b1e1e6 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,11 @@ BW_CLIENTID= BW_CLIENTSECRET= BW_PASSWORD= -# TOTP secret for the BW account's own 2FA (required if 2FA is enabled) +# Self-hosted Bitwarden/Vaultwarden server URL +BW_SERVER=https://pwvault.turnsys.com + +# TOTP secret for the BW account's own 2FA (optional — API key auth +# does not require TOTP; kept for backwards compatibility) BW_TOTP_SECRET= # Set to true for debugging (shows browser window — requires display) diff --git a/Dockerfile b/Dockerfile index e90e714..67afcd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,16 +4,24 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends \ jq \ unzip \ + wget \ + python3-pip \ libzbar0 \ libzbar-dev \ && rm -rf /var/lib/apt/lists/* -# Install Bitwarden CLI -RUN npm install -g @bitwarden/cli@2025.6.0 +# Install Bitwarden CLI — native Rust binary (no Node.js/npm) +# Same CLI interface as the npm package but zero runtime dependencies. +ARG BW_CLI_VERSION=2026.7.0 +RUN wget -q -O /tmp/bw.zip \ + "https://github.com/bitwarden/clients/releases/download/cli-v${BW_CLI_VERSION}/bw-linux-${BW_CLI_VERSION}.zip" && \ + unzip -o /tmp/bw.zip -d /usr/local/bin/ && \ + chmod +x /usr/local/bin/bw && \ + rm /tmp/bw.zip # Install Python dependencies COPY requirements.txt /tmp/ -RUN pip install --no-cache-dir -r /tmp/requirements.txt +RUN python3 -m pip install --no-cache-dir --break-system-packages -r /tmp/requirements.txt WORKDIR /app COPY . . diff --git a/bw-helper.py b/bw_helper.py similarity index 82% rename from bw-helper.py rename to bw_helper.py index ee4b537..d006025 100644 --- a/bw-helper.py +++ b/bw_helper.py @@ -22,11 +22,13 @@ from typing import Optional class BitwardenHelper: """Wrapper around the Bitwarden CLI for credential management.""" - def __init__(self, client_id: str, client_secret: str, password: str, totp_secret: str = ""): + def __init__(self, client_id: str, client_secret: str, password: str, + totp_secret: str = "", server_url: str = ""): self.client_id = client_id self.client_secret = client_secret self.password = password self.totp_secret = totp_secret + self.server_url = server_url self.session: Optional[str] = None def _run_bw(self, args: list[str], capture: bool = True) -> str: @@ -49,38 +51,41 @@ class BitwardenHelper: def login(self) -> None: """Authenticate via API key and unlock the vault. - If 2FA is enabled on the account, generates a TOTP code from - self.totp_secret and passes it via --code. + Configures the BW server URL (for self-hosted instances), logs in + via API key, and unlocks the vault. API key auth does not require + TOTP — the key itself is obtained from an authenticated session. """ env = os.environ.copy() env["BW_CLIENTID"] = self.client_id env["BW_CLIENTSECRET"] = self.client_secret - login_cmd = ["bw", "login", "--apikey"] - login_input = "" - - if self.totp_secret: - import pyotp - totp_code = pyotp.TOTP(self.totp_secret).now() - login_cmd += ["--code", totp_code] + # Configure server URL for self-hosted instances + if self.server_url: + subprocess.run( + ["bw", "config", "server", self.server_url], + capture_output=True, text=True, env=env, + ) + # Login via API key (tolerates already-logged-in state) result = subprocess.run( - login_cmd, - capture_output=True, - text=True, - env=env, - input=login_input, + ["bw", "login", "--apikey"], + capture_output=True, text=True, env=env, ) if result.returncode != 0 and "already" not in result.stderr.lower(): raise RuntimeError(f"BW login failed: {result.stderr.strip()}") - self.session = subprocess.run( - ["bw", "unlock", "--raw"], - capture_output=True, - text=True, - env=env, - input=self.password + "\n", - ).stdout.strip() + # Unlock via password file (more reliable than stdin with native binary) + import tempfile + with tempfile.NamedTemporaryFile(mode="w", suffix=".pw", delete=False) as pw_file: + pw_file.write(self.password) + pw_file_path = pw_file.name + try: + self.session = subprocess.run( + ["bw", "unlock", "--passwordfile", pw_file_path, "--raw"], + capture_output=True, text=True, env=env, + ).stdout.strip() + finally: + os.unlink(pw_file_path) if not self.session: raise RuntimeError("BW unlock failed — no session token returned") diff --git a/docker-compose.yml b/docker-compose.yml index 66e15d6..835b39e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: - BW_CLIENTID=${BW_CLIENTID} - BW_CLIENTSECRET=${BW_CLIENTSECRET} - BW_PASSWORD=${BW_PASSWORD} + - BW_SERVER=${BW_SERVER:-https://pwvault.turnsys.com} - BW_TOTP_SECRET=${BW_TOTP_SECRET:-} - HEADFUL=${HEADFUL:-false} volumes: diff --git a/provision-agent.py b/provision-agent.py index c9d9712..65189b6 100644 --- a/provision-agent.py +++ b/provision-agent.py @@ -620,6 +620,7 @@ def main(): client_secret=os.environ["BW_CLIENTSECRET"], password=os.environ["BW_PASSWORD"], totp_secret=os.environ.get("BW_TOTP_SECRET", ""), + server_url=os.environ.get("BW_SERVER", ""), ) log.info("Connecting to Bitwarden...") bw.login()