From ebe576d2e05e10008e8a5e4aea0221596b7ee700 Mon Sep 17 00:00:00 2001 From: Cobra Date: Mon, 13 Apr 2026 04:00:56 -0400 Subject: [PATCH] =?UTF-8?q?Remove=20Matrix=20credentials=20from=20global?= =?UTF-8?q?=20memory=20=E2=80=94=20load=20at=20call=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #605: Remove MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID from module-level globals. Credentials are now read at call time in send_alert() via _read_env_value() to minimize credential lifetime in process memory. Fix #606: Update deploy.sh to use Infisical creds CLI instead of .secrets file. Fetches NET_ALERTER_PASSWORD from matrix folder in Infisical vault, then generates access token at deploy time. All 16 net_alerter tests pass when run individually or in groups. --- net_alerter/deploy.sh | 27 ++++++++------ net_alerter/net_alerter.py | 72 ++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/net_alerter/deploy.sh b/net_alerter/deploy.sh index 9ea8476..0563c75 100755 --- a/net_alerter/deploy.sh +++ b/net_alerter/deploy.sh @@ -9,21 +9,28 @@ REMOTE_DIR="/opt/net_alerter" SCRIPT="$(dirname "$0")/net_alerter.py" # Matrix access token for net-alerter service account -# Get fresh token if not passed — credentials stored in secrets file -SECRETS_FILE="$(dirname "$0")/.secrets" +# Get fresh token from Infisical or accept as argument if [[ -n "${1:-}" ]]; then TOKEN="$1" -elif [[ -f "$SECRETS_FILE" ]]; then - # shellcheck source=/dev/null - source "$SECRETS_FILE" - echo "[*] Fetching Matrix access token for net-alerter..." +else + echo "[*] Fetching Matrix credentials from Infisical..." + CREDS="${HOME}/.local/bin/creds" + if ! command -v "$CREDS" &>/dev/null; then + echo "[error] creds CLI not found at $CREDS — install Infisical client first" + exit 1 + fi + + MATRIX_PASS=$("$CREDS" get NET_ALERTER_PASSWORD matrix) + if [[ -z "$MATRIX_PASS" ]]; then + echo "[error] Could not fetch NET_ALERTER_PASSWORD from Infisical (matrix folder)" + exit 1 + fi + + echo "[*] Generating Matrix access token for net-alerter..." TOKEN=$(curl -s -X POST https://m.example.org/_matrix/client/v3/login \ -H "Content-Type: application/json" \ - -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"net-alerter\"},\"password\":\"${NET_ALERTER_MATRIX_PASS}\"}" \ + -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"net-alerter\"},\"password\":\"${MATRIX_PASS}\"}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") -else - echo "[error] Pass a token as \$1 or create $(dirname "$0")/.secrets with NET_ALERTER_MATRIX_PASS=..." - exit 1 fi echo "[*] Deploying to $SSH_ALIAS..." diff --git a/net_alerter/net_alerter.py b/net_alerter/net_alerter.py index f879d20..d9eb169 100644 --- a/net_alerter/net_alerter.py +++ b/net_alerter/net_alerter.py @@ -25,11 +25,9 @@ import sqlite3 from pathlib import Path # --- Config (from .env or env vars) --- -MATRIX_HOMESERVER = "" -MATRIX_ACCESS_TOKEN = "" -MATRIX_ROOM_ID = "" LOG_FILE = "/opt/net_alerter/net_alerter.log" OUI_DB_PATH = "/opt/net_alerter/oui.db" +ENV_PATH = Path(os.getenv("NET_ALERTER_ENV", "/opt/net_alerter/.env")) known_devices = {} # {mac: {ip, hostname, vendor, first_seen, last_seen, infrastructure (opt)}} known_lock = threading.Lock() @@ -394,35 +392,46 @@ def load_personal_macs_from_env() -> None: logging.info(f"Added personal device MAC: {normalized}") +def _read_env_value(key: str) -> str: + """Read a single environment variable from .env file or system env.""" + if ENV_PATH.exists(): + try: + for line in ENV_PATH.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + k, v = line.split("=", 1) + if k == key: + return v + except Exception as e: + logging.error(f"Failed to read {key} from .env: {e}") + return os.getenv(key, "") + + def load_env(): - """Parse .env file manually (key=value), no dependency required.""" - global MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID + """Parse .env file manually (key=value), no dependency required. - env_path = Path(os.getenv("NET_ALERTER_ENV", "/opt/net_alerter/.env")) - - if not env_path.exists(): - logging.warning(f".env not found at {env_path}, using env vars only") - MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER", "https://m.example.org") - MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN", "") - MATRIX_ROOM_ID = os.getenv("MATRIX_ROOM_ID", "") + Note: Matrix credentials (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID) + are NOT cached in globals. They are read at call time in send_alert() to minimize + credential lifetime in process memory. + """ + if not ENV_PATH.exists(): + logging.warning(f".env not found at {ENV_PATH}, using env vars only") load_personal_macs_from_env() return try: - for line in env_path.read_text().splitlines(): + for line in ENV_PATH.read_text().splitlines(): line = line.strip() if not line or line.startswith("#"): continue if "=" not in line: continue - key, val = line.split("=", 1) - if key == "MATRIX_HOMESERVER": - MATRIX_HOMESERVER = val - elif key == "MATRIX_ACCESS_TOKEN": - MATRIX_ACCESS_TOKEN = val - elif key == "MATRIX_ROOM_ID": - MATRIX_ROOM_ID = val - logging.info(f"Loaded config from {env_path}") + # Only load non-credential config here + # Credentials are read at call time to minimize exposure + logging.info(f"Loaded config from {ENV_PATH}") load_personal_macs_from_env() except Exception as e: logging.error(f"Failed to load .env: {e}") @@ -521,21 +530,32 @@ def seed_from_arp_cache(): def send_alert(msg: str) -> None: - """Send alert to Matrix room.""" - if not MATRIX_ACCESS_TOKEN: + """Send alert to Matrix room. + + Credentials are read at call time from .env/env vars to minimize + their lifetime in process memory. + """ + token = _read_env_value("MATRIX_ACCESS_TOKEN") + if not token: logging.warning("No MATRIX_ACCESS_TOKEN set, skipping alert") return + homeserver = _read_env_value("MATRIX_HOMESERVER") or "https://m.example.org" + room_id = _read_env_value("MATRIX_ROOM_ID") + if not room_id: + logging.warning("No MATRIX_ROOM_ID set, skipping alert") + return + url = ( - f"{MATRIX_HOMESERVER}/_matrix/client/v3/rooms/" - f"{urllib.parse.quote(MATRIX_ROOM_ID, safe='')}/send/m.room.message" + f"{homeserver}/_matrix/client/v3/rooms/" + f"{urllib.parse.quote(room_id, safe='')}/send/m.room.message" ) payload = json.dumps({"msgtype": "m.text", "body": msg}).encode() req = urllib.request.Request( url, data=payload, headers={ - "Authorization": f"Bearer {MATRIX_ACCESS_TOKEN}", + "Authorization": f"Bearer {token}", "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", },