AlphornAlphorn Docs

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.

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):

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:

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:

/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:

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

ChannelFilterPurpose
PagerDutyTags has any of brute-forceImmediate response to attacks
Slack (#security)Tags has any of securityAll security events
TelegramTags has any of root-loginPersonal alert for root access
EmailTags has any of securityAudit trail

On this page