feat: replace Node.js bw CLI with native Rust binary + fix module name
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
This commit is contained in:
+5
-1
@@ -3,7 +3,11 @@ BW_CLIENTID=
|
|||||||
BW_CLIENTSECRET=
|
BW_CLIENTSECRET=
|
||||||
BW_PASSWORD=
|
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=
|
BW_TOTP_SECRET=
|
||||||
|
|
||||||
# Set to true for debugging (shows browser window — requires display)
|
# Set to true for debugging (shows browser window — requires display)
|
||||||
|
|||||||
+11
-3
@@ -4,16 +4,24 @@ RUN apt-get update && \
|
|||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
jq \
|
jq \
|
||||||
unzip \
|
unzip \
|
||||||
|
wget \
|
||||||
|
python3-pip \
|
||||||
libzbar0 \
|
libzbar0 \
|
||||||
libzbar-dev \
|
libzbar-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Bitwarden CLI
|
# Install Bitwarden CLI — native Rust binary (no Node.js/npm)
|
||||||
RUN npm install -g @bitwarden/cli@2025.6.0
|
# 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
|
# Install Python dependencies
|
||||||
COPY requirements.txt /tmp/
|
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
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
+27
-22
@@ -22,11 +22,13 @@ from typing import Optional
|
|||||||
class BitwardenHelper:
|
class BitwardenHelper:
|
||||||
"""Wrapper around the Bitwarden CLI for credential management."""
|
"""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_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
self.password = password
|
self.password = password
|
||||||
self.totp_secret = totp_secret
|
self.totp_secret = totp_secret
|
||||||
|
self.server_url = server_url
|
||||||
self.session: Optional[str] = None
|
self.session: Optional[str] = None
|
||||||
|
|
||||||
def _run_bw(self, args: list[str], capture: bool = True) -> str:
|
def _run_bw(self, args: list[str], capture: bool = True) -> str:
|
||||||
@@ -49,38 +51,41 @@ class BitwardenHelper:
|
|||||||
def login(self) -> None:
|
def login(self) -> None:
|
||||||
"""Authenticate via API key and unlock the vault.
|
"""Authenticate via API key and unlock the vault.
|
||||||
|
|
||||||
If 2FA is enabled on the account, generates a TOTP code from
|
Configures the BW server URL (for self-hosted instances), logs in
|
||||||
self.totp_secret and passes it via --code.
|
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 = os.environ.copy()
|
||||||
env["BW_CLIENTID"] = self.client_id
|
env["BW_CLIENTID"] = self.client_id
|
||||||
env["BW_CLIENTSECRET"] = self.client_secret
|
env["BW_CLIENTSECRET"] = self.client_secret
|
||||||
|
|
||||||
login_cmd = ["bw", "login", "--apikey"]
|
# Configure server URL for self-hosted instances
|
||||||
login_input = ""
|
if self.server_url:
|
||||||
|
subprocess.run(
|
||||||
if self.totp_secret:
|
["bw", "config", "server", self.server_url],
|
||||||
import pyotp
|
capture_output=True, text=True, env=env,
|
||||||
totp_code = pyotp.TOTP(self.totp_secret).now()
|
)
|
||||||
login_cmd += ["--code", totp_code]
|
|
||||||
|
|
||||||
|
# Login via API key (tolerates already-logged-in state)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
login_cmd,
|
["bw", "login", "--apikey"],
|
||||||
capture_output=True,
|
capture_output=True, text=True, env=env,
|
||||||
text=True,
|
|
||||||
env=env,
|
|
||||||
input=login_input,
|
|
||||||
)
|
)
|
||||||
if result.returncode != 0 and "already" not in result.stderr.lower():
|
if result.returncode != 0 and "already" not in result.stderr.lower():
|
||||||
raise RuntimeError(f"BW login failed: {result.stderr.strip()}")
|
raise RuntimeError(f"BW login failed: {result.stderr.strip()}")
|
||||||
|
|
||||||
self.session = subprocess.run(
|
# Unlock via password file (more reliable than stdin with native binary)
|
||||||
["bw", "unlock", "--raw"],
|
import tempfile
|
||||||
capture_output=True,
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".pw", delete=False) as pw_file:
|
||||||
text=True,
|
pw_file.write(self.password)
|
||||||
env=env,
|
pw_file_path = pw_file.name
|
||||||
input=self.password + "\n",
|
try:
|
||||||
).stdout.strip()
|
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:
|
if not self.session:
|
||||||
raise RuntimeError("BW unlock failed — no session token returned")
|
raise RuntimeError("BW unlock failed — no session token returned")
|
||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
- BW_CLIENTID=${BW_CLIENTID}
|
- BW_CLIENTID=${BW_CLIENTID}
|
||||||
- BW_CLIENTSECRET=${BW_CLIENTSECRET}
|
- BW_CLIENTSECRET=${BW_CLIENTSECRET}
|
||||||
- BW_PASSWORD=${BW_PASSWORD}
|
- BW_PASSWORD=${BW_PASSWORD}
|
||||||
|
- BW_SERVER=${BW_SERVER:-https://pwvault.turnsys.com}
|
||||||
- BW_TOTP_SECRET=${BW_TOTP_SECRET:-}
|
- BW_TOTP_SECRET=${BW_TOTP_SECRET:-}
|
||||||
- HEADFUL=${HEADFUL:-false}
|
- HEADFUL=${HEADFUL:-false}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ def main():
|
|||||||
client_secret=os.environ["BW_CLIENTSECRET"],
|
client_secret=os.environ["BW_CLIENTSECRET"],
|
||||||
password=os.environ["BW_PASSWORD"],
|
password=os.environ["BW_PASSWORD"],
|
||||||
totp_secret=os.environ.get("BW_TOTP_SECRET", ""),
|
totp_secret=os.environ.get("BW_TOTP_SECRET", ""),
|
||||||
|
server_url=os.environ.get("BW_SERVER", ""),
|
||||||
)
|
)
|
||||||
log.info("Connecting to Bitwarden...")
|
log.info("Connecting to Bitwarden...")
|
||||||
bw.login()
|
bw.login()
|
||||||
|
|||||||
Reference in New Issue
Block a user