# Security Alerts

> Get security alerts for failed SSH logins, root and sudo access, and application auth events, routed by priority to Slack, email, or SMS.

Source: https://docs.alphorn.dev/use-cases/security-alerts/

Security events need fast response. Route login failures, SSH access, and suspicious activity through Alphorn so the right people are alerted immediately.

## Failed SSH login attempts

Count failed SSH logins from the systemd journal (the SSH unit is `ssh` on Debian/Ubuntu and `sshd` on RHEL/Fedora, so the script asks for both). Run it every 10 minutes from root's crontab (reading the journal needs root or the `systemd-journal` group):

```bash title="check-ssh-failures.sh"
#!/bin/bash
WEBHOOK="https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT"
ALPHORN_API_KEY="${ALPHORN_API_KEY:-alp_...}"  # or export it in the environment
HOSTNAME=$(hostname)
THRESHOLD=5

# Failed attempts in the last 10 minutes
LINES=$(journalctl -u ssh -u sshd --since "10 min ago" -o cat --no-pager | grep "Failed password")
FAILURES=$(printf '%s' "$LINES" | grep -c "Failed password")

if [ "$FAILURES" -ge "$THRESHOLD" ]; then
  # "Failed password for <user> from <ip> port <port> ssh2": the IP is 4th from the end
  SOURCES=$(printf '%s\n' "$LINES" | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -5)

  # jq escapes the multi-line source list into valid JSON
  jq -n --arg h "$HOSTNAME" --arg f "$FAILURES" --arg s "$SOURCES" \
    '{title: "SSH brute force detected: \($h)", message: "\($f) failed login attempts in the last 10 minutes.\n\nTop sources:\n\($s)", priority: 5, tags: ["security", "ssh", "brute-force", $h]}' |
  curl -fsS -X POST "$WEBHOOK" \
    -H "Authorization: Bearer $ALPHORN_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @-
fi
```

This counts password failures. On servers with `PasswordAuthentication no`, attackers show up as "Invalid user" or "Connection closed by authenticating user" instead - add those patterns to the `grep` (e.g. `grep -E "Failed password|Invalid user"`) and adjust the IP extraction.

## Successful root/sudo access

Alert whenever someone starts a root login shell (direct root login, `su -`, `sudo -i`). Files in `/etc/profile.d` are sourced by login shells (on RHEL/Fedora also by interactive shells like `sudo -s`), so a plain `sudo cmd` doesn't trigger it. Store the key in a root-only file first:

```bash
echo 'ALPHORN_API_KEY=alp_...' | sudo tee /etc/alphorn.env >/dev/null
sudo chmod 600 /etc/alphorn.env
```

`/etc/profile.d` is sourced by `sh`, so the script sticks to POSIX syntax:

```sh title="/etc/profile.d/notify-root.sh"
if [ "$(id -u)" -eq 0 ] && [ -r /etc/alphorn.env ]; then
  . /etc/alphorn.env
  curl -fsS -X POST "https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT" \
    -H "Authorization: Bearer $ALPHORN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"title\": \"Root login: $(hostname)\",
      \"message\": \"User: $(logname 2>/dev/null), IP: ${SSH_CLIENT%% *}, TTY: $(tty)\",
      \"priority\": 4,
      \"tags\": [\"security\", \"root-login\", \"$(hostname)\"]
    }" >/dev/null 2>&1 &
fi
```

## Application-level auth alerts

Notify on suspicious login patterns from your app:

```javascript
async function onLoginFailed(email, ip, attempts) {
  if (attempts >= 5) {
    await fetch("https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.ALPHORN_API_KEY}`,
      },
      body: JSON.stringify({
        title: `Account lockout: ${email}`,
        message: `${attempts} failed login attempts from IP ${ip}`,
        priority: 4,
        tags: ["security", "login-failed", "lockout"],
      }),
    });
  }
}

async function onLoginFromNewDevice(user, device, ip, location) {
  await fetch("https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.ALPHORN_API_KEY}`,
    },
    body: JSON.stringify({
      title: `New device login: ${user.email}`,
      message: `Device: ${device}\nIP: ${ip}\nLocation: ${location}`,
      priority: 3,
      tags: ["security", "new-device"],
    }),
  });
}
```

## Routing examples

| Channel | Filter | Purpose |
|---|---|---|
| PagerDuty | Tags has any of `brute-force` | Immediate response to attacks |
| Slack (#security) | Tags has any of `security` | All security events |
| Telegram | Tags has any of `root-login` | Personal alert for root access |
| Email | Tags has any of `security` | Audit trail |
