mirror of
https://forgejo.altau.su/lego/lego-monitoring.git
synced 2026-03-10 04:41:10 +00:00
delayed login alerts to prevent spam with mass logins
This commit is contained in:
parent
0e177210f6
commit
b1b06b2e51
8 changed files with 100 additions and 7 deletions
|
|
@ -12,6 +12,7 @@ DISCLAIMER: This repository does not have anything to do with the LEGO Group. "l
|
||||||
* Invite the bot account to the room (you have to accept the invite manually)
|
* Invite the bot account to the room (you have to accept the invite manually)
|
||||||
* Copy `config.example.json` to `config.json`, edit as necessary
|
* Copy `config.example.json` to `config.json`, edit as necessary
|
||||||
* Run `alerting/login.py` once to login into Matrix
|
* Run `alerting/login.py` once to login into Matrix
|
||||||
|
* (optional) Create an `alerting` group and give `config.json`'s ownership and read rights to it, to allow sending alerts from less-privileged users
|
||||||
|
|
||||||
### Setting up login alerts
|
### Setting up login alerts
|
||||||
|
|
||||||
|
|
|
||||||
53
alerting/delayed.py
Normal file
53
alerting/delayed.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from alerting.alerts import Alert, send_alert
|
||||||
|
from misc.common import TMP_DIR
|
||||||
|
|
||||||
|
handler_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
async def handler():
|
||||||
|
"""
|
||||||
|
Is executed when SIGUSR1 is received.
|
||||||
|
Checks for new alerts, sends them in 10 seconds
|
||||||
|
"""
|
||||||
|
alert_counts: list[list] = []
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
async with handler_lock:
|
||||||
|
alert_files = [f for f in os.listdir(str(TMP_DIR)) if f.startswith("alert-")]
|
||||||
|
for fname in alert_files:
|
||||||
|
fpath = str(TMP_DIR / fname)
|
||||||
|
with open(fpath) as f:
|
||||||
|
alert = Alert(**json.load(f))
|
||||||
|
for pair in alert_counts:
|
||||||
|
if alert == pair[0]:
|
||||||
|
pair[1] += 1
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
alert_counts.append([alert, 1])
|
||||||
|
os.unlink(fpath)
|
||||||
|
for pair in alert_counts:
|
||||||
|
if pair[1] > 1:
|
||||||
|
pair[0].message += f" ({pair[1]} times)"
|
||||||
|
pair[0].html_message += f" ({pair[1]} times)"
|
||||||
|
logging.info(f"Sending delayed alert of type {pair[0].alert_type}")
|
||||||
|
await send_alert(pair[0])
|
||||||
|
|
||||||
|
|
||||||
|
def send_alert_delayed(alert: Alert) -> None:
|
||||||
|
"""
|
||||||
|
Put alert into a temporary file to be sent later.
|
||||||
|
Intended as an IPC mechanism
|
||||||
|
"""
|
||||||
|
alert_json = json.dumps(asdict(alert))
|
||||||
|
with tempfile.NamedTemporaryFile(prefix="alert-", dir=str(TMP_DIR), delete=False) as af:
|
||||||
|
af.write(alert_json.encode())
|
||||||
|
with open(str(TMP_DIR / "pid")) as pf:
|
||||||
|
pid = int(pf.read().strip())
|
||||||
|
os.kill(pid, signal.SIGUSR1)
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
Defaults env_keep += "SSH_CLIENT"
|
Defaults env_keep += "SSH_CLIENT"
|
||||||
ALL ALL=(ALL:ALL) NOPASSWD: /opt/lego-monitoring/wrappers/send_login_alert.sh
|
ALL ALL=(ALL:ALL) NOPASSWD:SETENV: /opt/lego-monitoring/wrappers/send_login_alert.sh
|
||||||
|
ALL ALL=(ALL:ALL) NOPASSWD:SETENV: /opt/lego-monitoring/wrappers/send_local_login_alert.sh
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
CONFIG_FILE = (Path(os.path.dirname(os.path.realpath(__file__))) / ".." / "config.json").resolve()
|
CONFIG_FILE = (Path(os.path.dirname(os.path.realpath(__file__))) / ".." / "config.json").resolve()
|
||||||
|
TMP_DIR = Path(tempfile.gettempdir()) / "lego-monitoring"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from alerting import alerts
|
from alerting import alerts
|
||||||
|
from alerting.delayed import send_alert_delayed
|
||||||
from alerting.enum import AlertType, Severity
|
from alerting.enum import AlertType, Severity
|
||||||
from misc.config import get_config
|
from misc.config import get_config
|
||||||
|
|
||||||
|
|
@ -14,33 +17,37 @@ async def main():
|
||||||
try:
|
try:
|
||||||
from_where = os.environ["SSH_CLIENT"].split()[0]
|
from_where = os.environ["SSH_CLIENT"].split()[0]
|
||||||
except:
|
except:
|
||||||
from_where = os.ttyname(sys.stdout.fileno())
|
from_where = "localhost"
|
||||||
is_local = True
|
is_local = True
|
||||||
else:
|
else:
|
||||||
is_local = False
|
is_local = False
|
||||||
|
|
||||||
|
if not is_local and len(sys.argv) > 1 and sys.argv[1] == "local-only":
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actual_user = os.environ["SUDO_USER"]
|
actual_user = os.environ["SUDO_USER"]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await alerts.send_alert(
|
await alerts.send_alert(
|
||||||
alerts.Alert(
|
alerts.Alert(
|
||||||
alert_type=AlertType.ERROR,
|
alert_type=AlertType.ERROR,
|
||||||
message=f"Failed to determine username for login from {from_where}, see logs",
|
message=f"Failed to determine username for login from {from_where}: {repr(exc)}, see logs",
|
||||||
severity=Severity.CRITICAL,
|
severity=Severity.CRITICAL,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
return
|
return
|
||||||
|
|
||||||
if not is_local:
|
if not is_local:
|
||||||
rdns_result = socket.getnameinfo((from_where, 0), 0)[0]
|
rdns_result = socket.getnameinfo((from_where, 0), 0)[0]
|
||||||
message = f"Login from {from_where} as {actual_user} on `{check_config.hostname}`"
|
message = f"Login from {from_where} as {actual_user} on {check_config.hostname}"
|
||||||
html_message = f"Login from <code>{from_where}</code> ({rdns_result}) as {actual_user} on <code>{check_config.hostname}</code>"
|
html_message = f"Login from <code>{from_where}</code> ({rdns_result}) as {actual_user} on <code>{check_config.hostname}</code>"
|
||||||
else:
|
else:
|
||||||
message = f"Login from {from_where} as {actual_user} on {check_config.hostname}"
|
message = f"Login from {from_where} as {actual_user} on {check_config.hostname}"
|
||||||
html_message = f"Login from {from_where} as {actual_user} on <code>{check_config.hostname}</code>"
|
html_message = f"Login from {from_where} as {actual_user} on <code>{check_config.hostname}</code>"
|
||||||
|
|
||||||
alert = alerts.Alert(alert_type=AlertType.LOGIN, message=message, severity=Severity.INFO, html_message=html_message)
|
alert = alerts.Alert(alert_type=AlertType.LOGIN, message=message, severity=Severity.INFO, html_message=html_message)
|
||||||
await alerts.send_alert(alert)
|
send_alert_delayed(alert)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
25
service.py
25
service.py
|
|
@ -2,11 +2,14 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from alerting import alerts
|
from alerting import alerts, delayed
|
||||||
from misc import checks, cvars
|
from misc import checks, cvars
|
||||||
from misc.checkers import interval_checker, scheduled_checker
|
from misc.checkers import interval_checker, scheduled_checker
|
||||||
|
from misc.common import TMP_DIR
|
||||||
from misc.config import get_config
|
from misc.config import get_config
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
@ -19,9 +22,28 @@ def stop_gracefully(signum, frame):
|
||||||
stopping = True
|
stopping = True
|
||||||
|
|
||||||
|
|
||||||
|
def create_temp_dir() -> None:
|
||||||
|
try:
|
||||||
|
os.mkdir(str(TMP_DIR))
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
os.chmod(str(TMP_DIR), 0o770)
|
||||||
|
try:
|
||||||
|
shutil.chown(str(TMP_DIR), user="root", group="alerting")
|
||||||
|
except LookupError:
|
||||||
|
shutil.chown(str(TMP_DIR), user="root", group="root")
|
||||||
|
with open(str(TMP_DIR / "pid"), "w") as pf:
|
||||||
|
pf.write(str(os.getpid()))
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
create_temp_dir()
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, stop_gracefully)
|
signal.signal(signal.SIGTERM, stop_gracefully)
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.add_signal_handler(signal.SIGUSR1, lambda: asyncio.create_task(delayed.handler()))
|
||||||
|
|
||||||
cvars.config.set(get_config())
|
cvars.config.set(get_config())
|
||||||
|
|
||||||
client = await alerts.get_client()
|
client = await alerts.get_client()
|
||||||
|
|
@ -46,6 +68,7 @@ async def main():
|
||||||
while True:
|
while True:
|
||||||
if stopping:
|
if stopping:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
shutil.rmtree(str(TMP_DIR))
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ mydir=$(dirname "$0")
|
||||||
sudo "$mydir/send_login_alert.sh"
|
sudo "$mydir/send_login_alert.sh"
|
||||||
|
|
||||||
shell=$(getent passwd $LOGNAME | cut -d: -f7)
|
shell=$(getent passwd $LOGNAME | cut -d: -f7)
|
||||||
if [[ -n $SSH_ORIGINAL_COMMAND ]] # command given, so run it
|
if [ "$SSH_ORIGINAL_COMMAND" = "internal-sftp" ] # command given, so run it
|
||||||
then
|
then
|
||||||
|
exec /usr/lib/ssh/sftp-server
|
||||||
|
elif [[ -n $SSH_ORIGINAL_COMMAND ]]; then
|
||||||
exec "$shell" -c "$SSH_ORIGINAL_COMMAND"
|
exec "$shell" -c "$SSH_ORIGINAL_COMMAND"
|
||||||
else # no command, so interactive login shell
|
else # no command, so interactive login shell
|
||||||
exec "$shell" -il
|
exec "$shell" -il
|
||||||
|
|
|
||||||
4
wrappers/send_local_login_alert.sh
Executable file
4
wrappers/send_local_login_alert.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mydir=$(dirname "$0")
|
||||||
|
"$mydir/../.venv/bin/python" "$mydir/../send_login_alert.py" "local-only"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue