Remove Matrix credentials from global memory — load at call time

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.
This commit is contained in:
Cobra
2026-04-13 04:00:56 -04:00
parent c87967f2c4
commit 13900f9045
2 changed files with 63 additions and 36 deletions
+46 -26
View File
@@ -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",
},