From 8f7cdc2a38e8f6fd47f2d8142210a48c4620eb8d Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 16 May 2026 19:46:41 +0200 Subject: [PATCH 1/2] refactor(entrypoint): use standard .env pattern instead of in-container bw Removes the bw CLI from the image and the entrypoint that fetched GUARD_PRIVATE_KEY at container start. The infrastructure repo now provides GUARD_PRIVATE_KEY directly via .env (generated by generate-env.sh from Vaultwarden at deploy time), consistent with how every other secret is wired into this stack. Trade-off: GUARD_PRIVATE_KEY now lives on the host's .env file (chmod 600) between deploys; redeploy is required to rotate it. The added complexity of bw-in-container was not worth the divergence from the established pattern. --- Dockerfile | 8 ------- entrypoint.sh | 62 --------------------------------------------------- 2 files changed, 70 deletions(-) delete mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 66ec1cd..9fbda16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,5 @@ FROM node:lts-alpine -# bw CLI is used by entrypoint.sh to fetch GUARD_PRIVATE_KEY from Vaultwarden at container start. -# Installed as root before switching user so the global npm prefix is writable. -RUN apk add --no-cache bash && npm install -g @bitwarden/cli@2024.9.0 - RUN mkdir /app && chown -R node:node /app WORKDIR /app USER node @@ -17,11 +13,7 @@ COPY --chown=node . . RUN npm run prisma:generate RUN npm run build -# Entrypoint optionally fetches GUARD_PRIVATE_KEY from Vaultwarden, then execs npm. -COPY --chown=node --chmod=0755 entrypoint.sh /app/entrypoint.sh - # Expose port EXPOSE 3001 -ENTRYPOINT ["/app/entrypoint.sh"] CMD ["npm", "run", "start:migrate"] diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 7ed051d..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -# entrypoint.sh — Optionally fetches GUARD_PRIVATE_KEY from Vaultwarden at container -# start (not deploy time), so the key only ever lives in container memory. -# -# Activation is opt-in: set VAULT_FETCH_GUARD_KEY=true and provide: -# VAULT_ITEM_NAME Name of the vault item holding the key -# VAULT_PASSWORD_FILE Path to file with the master password -# VAULT_SERVER_URL Bitwarden server URL (default: https://dfxvault.com) -# VAULT_FIELD Item field to read (default: GUARD_PRIVATE_KEY) -# -# When GUARD_ENABLED=true and VAULT_FETCH_GUARD_KEY=true the entrypoint fails fast -# if the vault is unreachable or the item is missing — the service must not silently -# start without a signing key. - -set -e - -if [ "${VAULT_FETCH_GUARD_KEY:-false}" = "true" ]; then - : "${VAULT_ITEM_NAME:?VAULT_ITEM_NAME is required when VAULT_FETCH_GUARD_KEY=true}" - : "${VAULT_PASSWORD_FILE:?VAULT_PASSWORD_FILE is required when VAULT_FETCH_GUARD_KEY=true}" - : "${VAULT_BW_DATA_DIR:?VAULT_BW_DATA_DIR is required when VAULT_FETCH_GUARD_KEY=true (mounted from host)}" - - VAULT_SERVER_URL="${VAULT_SERVER_URL:-https://dfxvault.com}" - VAULT_FIELD="${VAULT_FIELD:-GUARD_PRIVATE_KEY}" - - if [ ! -r "$VAULT_PASSWORD_FILE" ]; then - echo "entrypoint: vault password file $VAULT_PASSWORD_FILE not readable" >&2 - exit 1 - fi - - # Copy bw data to a writable per-container location so the host's bw state stays untouched - # even if `bw sync` updates the local cache. Without this, two containers sharing the same - # mount would race on data.json writes. - BW_RUNTIME_DIR="$(mktemp -d /tmp/bw-runtime.XXXXXX)" - cp -r "$VAULT_BW_DATA_DIR"/* "$BW_RUNTIME_DIR"/ 2>/dev/null || true - export BITWARDENCLI_APPDATA_DIR="$BW_RUNTIME_DIR" - - echo "entrypoint: configuring bw server $VAULT_SERVER_URL" - bw config server "$VAULT_SERVER_URL" > /dev/null - - echo "entrypoint: unlocking vault" - BW_PASSWORD="$(cat "$VAULT_PASSWORD_FILE")" - export BW_PASSWORD - BW_SESSION="$(bw unlock --passwordenv BW_PASSWORD --raw)" - export BW_SESSION - unset BW_PASSWORD - - echo "entrypoint: syncing vault" - bw sync --session "$BW_SESSION" > /dev/null - - echo "entrypoint: fetching $VAULT_FIELD from item '$VAULT_ITEM_NAME'" - GUARD_PRIVATE_KEY="$(bw get item "$VAULT_ITEM_NAME" --session "$BW_SESSION" \ - | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const i=JSON.parse(s);const f=(i.fields||[]).find(x=>x.name===process.argv[1]);if(!f){process.stderr.write("field not found: "+process.argv[1]+"\n");process.exit(2);}process.stdout.write(f.value);}' "$VAULT_FIELD")" - export GUARD_PRIVATE_KEY - - bw lock --session "$BW_SESSION" > /dev/null || true - rm -rf "$BW_RUNTIME_DIR" - unset BW_SESSION BITWARDENCLI_APPDATA_DIR - - echo "entrypoint: GUARD_PRIVATE_KEY loaded into env" -fi - -exec "$@" From 84c2e6db2acbd6f77249d8e0f35cba2da0f91e28 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 16 May 2026 20:35:28 +0200 Subject: [PATCH 2/2] docs(minter-guard): document env vars + fix stale whitelist comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env.example: document GUARD_ENABLED, GUARD_PRIVATE_KEY, GUARD_HELPER_ADDRESS, GUARD_WHITELIST_FILE in the same style as the other sections - README.md: add Minter Guard to the architecture list - whitelist.{testnet,mainnet}.json: rewrite stale "_comment". The service denies bridge proposals too — the previous comment promised the opposite. --- .env.example | 21 +++++++++++++++++++ README.md | 1 + .../config/whitelist.mainnet.json | 2 +- .../config/whitelist.testnet.json | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b2e7809..4b4ea44 100644 --- a/.env.example +++ b/.env.example @@ -59,3 +59,24 @@ GECKOTERMINAL_BASE_URL=http://pricing-proxy:8080/geckoterminal # observes when the same chat receives alerts from several chains. Leave # unset on single-chain stacks. Free-form value, rendered upper-cased. # CHAIN=Mainnet + +# Minter-guard auto-deny watcher. +# +# When enabled, the watcher iterates PROPOSED minters at the end of every +# monitoring cycle and submits denyMinter() for any address not on the +# committed whitelist (src/monitoringV2/config/whitelist.{testnet,mainnet}.json). +# Bridge proposals are not exempted: the bridge type is inferred from a trivial +# usd() view call and is therefore unsafe to exclude. +# +# GUARD_ENABLED true/false. Disables the watcher entirely if false. +# GUARD_PRIVATE_KEY Hex private key (0x...) of the signer. Must hold or be +# delegated enough voting power to pass checkQualified() +# on the JUSD reserve. +# GUARD_HELPER_ADDRESS Address passed as the single helper to denyMinter(). +# Use the equity holder that delegated to the signer. +# GUARD_WHITELIST_FILE Absolute path to the whitelist JSON inside the +# container (e.g. /app/src/monitoringV2/config/whitelist.mainnet.json). +# GUARD_ENABLED=false +# GUARD_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 +# GUARD_HELPER_ADDRESS=0x0000000000000000000000000000000000000000 +# GUARD_WHITELIST_FILE=/app/src/monitoringV2/config/whitelist.mainnet.json diff --git a/README.md b/README.md index 11738cb..faebf60 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The monitoring service continuously syncs blockchain data to provide real-time i - Collateral aggregation by token type 4. **Token Prices**: Fetches real-time prices from GeckoTerminal API with caching 5. **API Endpoints**: Serves data via REST API for frontend consumption +6. **Minter Guard**: Optional auto-deny watcher (opt-in via `GUARD_ENABLED=true`). At the end of every monitoring cycle it submits `denyMinter()` for any `PROPOSED` minter not on a committed whitelist (`src/monitoringV2/config/whitelist.{testnet,mainnet}.json`). Requires `GUARD_PRIVATE_KEY` and `GUARD_HELPER_ADDRESS`. See `.env.example`. ## Tech Stack diff --git a/src/monitoringV2/config/whitelist.mainnet.json b/src/monitoringV2/config/whitelist.mainnet.json index 98bc0c4..abdb6ec 100644 --- a/src/monitoringV2/config/whitelist.mainnet.json +++ b/src/monitoringV2/config/whitelist.mainnet.json @@ -1,4 +1,4 @@ { - "_comment": "Whitelist of approved generic minters (lowercase addresses). Empty = deny any new minter proposal. Bridges are never auto-denied (different type). Update via PR and redeploy.", + "_comment": "Whitelist of approved minter addresses (lowercase). Empty = deny any new minter proposal, including bridges (bridge type is inferred from a trivial usd() view call and is therefore unsafe to exempt). Add legitimate proposals here via PR + redeploy before they pass the application period.", "minters": [] } diff --git a/src/monitoringV2/config/whitelist.testnet.json b/src/monitoringV2/config/whitelist.testnet.json index 98bc0c4..abdb6ec 100644 --- a/src/monitoringV2/config/whitelist.testnet.json +++ b/src/monitoringV2/config/whitelist.testnet.json @@ -1,4 +1,4 @@ { - "_comment": "Whitelist of approved generic minters (lowercase addresses). Empty = deny any new minter proposal. Bridges are never auto-denied (different type). Update via PR and redeploy.", + "_comment": "Whitelist of approved minter addresses (lowercase). Empty = deny any new minter proposal, including bridges (bridge type is inferred from a trivial usd() view call and is therefore unsafe to exempt). Add legitimate proposals here via PR + redeploy before they pass the application period.", "minters": [] }