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:
+17
-10
@@ -9,21 +9,28 @@ REMOTE_DIR="/opt/net_alerter"
|
|||||||
SCRIPT="$(dirname "$0")/net_alerter.py"
|
SCRIPT="$(dirname "$0")/net_alerter.py"
|
||||||
|
|
||||||
# Matrix access token for net-alerter service account
|
# Matrix access token for net-alerter service account
|
||||||
# Get fresh token if not passed — credentials stored in secrets file
|
# Get fresh token from Infisical or accept as argument
|
||||||
SECRETS_FILE="$(dirname "$0")/.secrets"
|
|
||||||
if [[ -n "${1:-}" ]]; then
|
if [[ -n "${1:-}" ]]; then
|
||||||
TOKEN="$1"
|
TOKEN="$1"
|
||||||
elif [[ -f "$SECRETS_FILE" ]]; then
|
else
|
||||||
# shellcheck source=/dev/null
|
echo "[*] Fetching Matrix credentials from Infisical..."
|
||||||
source "$SECRETS_FILE"
|
CREDS="${HOME}/.local/bin/creds"
|
||||||
echo "[*] Fetching Matrix access token for net-alerter..."
|
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 \
|
TOKEN=$(curl -s -X POST https://m.example.org/_matrix/client/v3/login \
|
||||||
-H "Content-Type: application/json" \
|
-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'])")
|
| 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
|
fi
|
||||||
|
|
||||||
echo "[*] Deploying to $SSH_ALIAS..."
|
echo "[*] Deploying to $SSH_ALIAS..."
|
||||||
|
|||||||
+51
-31
@@ -25,11 +25,9 @@ import sqlite3
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# --- Config (from .env or env vars) ---
|
# --- Config (from .env or env vars) ---
|
||||||
MATRIX_HOMESERVER = ""
|
|
||||||
MATRIX_ACCESS_TOKEN = ""
|
|
||||||
MATRIX_ROOM_ID = ""
|
|
||||||
LOG_FILE = "/opt/net_alerter/net_alerter.log"
|
LOG_FILE = "/opt/net_alerter/net_alerter.log"
|
||||||
OUI_DB_PATH = "/opt/net_alerter/oui.db"
|
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_devices = {} # {mac: {ip, hostname, vendor, first_seen, last_seen, infrastructure (opt)}}
|
||||||
known_lock = threading.Lock()
|
known_lock = threading.Lock()
|
||||||
@@ -394,35 +392,46 @@ def load_personal_macs_from_env() -> None:
|
|||||||
logging.info(f"Added personal device MAC: {normalized}")
|
logging.info(f"Added personal device MAC: {normalized}")
|
||||||
|
|
||||||
|
|
||||||
def load_env():
|
def _read_env_value(key: str) -> str:
|
||||||
"""Parse .env file manually (key=value), no dependency required."""
|
"""Read a single environment variable from .env file or system env."""
|
||||||
global MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID
|
if ENV_PATH.exists():
|
||||||
|
|
||||||
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", "")
|
|
||||||
load_personal_macs_from_env()
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for line in env_path.read_text().splitlines():
|
for line in ENV_PATH.read_text().splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
key, val = line.split("=", 1)
|
k, v = line.split("=", 1)
|
||||||
if key == "MATRIX_HOMESERVER":
|
if k == key:
|
||||||
MATRIX_HOMESERVER = val
|
return v
|
||||||
elif key == "MATRIX_ACCESS_TOKEN":
|
except Exception as e:
|
||||||
MATRIX_ACCESS_TOKEN = val
|
logging.error(f"Failed to read {key} from .env: {e}")
|
||||||
elif key == "MATRIX_ROOM_ID":
|
return os.getenv(key, "")
|
||||||
MATRIX_ROOM_ID = val
|
|
||||||
logging.info(f"Loaded config from {env_path}")
|
|
||||||
|
def load_env():
|
||||||
|
"""Parse .env file manually (key=value), no dependency required.
|
||||||
|
|
||||||
|
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():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
# 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()
|
load_personal_macs_from_env()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to load .env: {e}")
|
logging.error(f"Failed to load .env: {e}")
|
||||||
@@ -521,21 +530,32 @@ def seed_from_arp_cache():
|
|||||||
|
|
||||||
|
|
||||||
def send_alert(msg: str) -> None:
|
def send_alert(msg: str) -> None:
|
||||||
"""Send alert to Matrix room."""
|
"""Send alert to Matrix room.
|
||||||
if not MATRIX_ACCESS_TOKEN:
|
|
||||||
|
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")
|
logging.warning("No MATRIX_ACCESS_TOKEN set, skipping alert")
|
||||||
return
|
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 = (
|
url = (
|
||||||
f"{MATRIX_HOMESERVER}/_matrix/client/v3/rooms/"
|
f"{homeserver}/_matrix/client/v3/rooms/"
|
||||||
f"{urllib.parse.quote(MATRIX_ROOM_ID, safe='')}/send/m.room.message"
|
f"{urllib.parse.quote(room_id, safe='')}/send/m.room.message"
|
||||||
)
|
)
|
||||||
payload = json.dumps({"msgtype": "m.text", "body": msg}).encode()
|
payload = json.dumps({"msgtype": "m.text", "body": msg}).encode()
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
data=payload,
|
data=payload,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {MATRIX_ACCESS_TOKEN}",
|
"Authorization": f"Bearer {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user