Lightweight SSH login observability for Linux servers. Sends a rich push notification via ntfy on every SSH login and failed attempt, and shows a live security banner at login time.
seclog-linux uses a unified push model: successful interactive SSH logins
and failed authentication events both end up in the same ntfy topic, so you
have one notification stream for SSH visibility instead of separate tooling.
On every successful SSH login, the user sees a colored banner:
── Currently active SSH connections (2) ──
192.168.1.100 54123 alice 2026-04-18 19:46
203.0.113.55 52401 bob 2026-04-18 19:30
── Last 5 successful logins (distinct IPs) ──
Apr 18 19:46:12 alice 192.168.1.100
Apr 18 18:22:01 alice 203.0.113.55
...
── ⚠ Failed SSH attempts (24 hours ago): 37 from 4 IP(s) ──
23x Apr 18 03:14:00 45.134.26.12 user=root
8x Apr 17 22:01:15 91.200.12.3 user=admin
...
And a push to your phone:
SSH login: alice@myserver from 192.168.1.100
──────────────────────────────────────────
User: alice (uid=1000) (sudo)
From: 192.168.1.100:54123
Host: laptop.local
Auth: publickey ED25519
Key: SHA256:pbtcot6DtoyQGtTk...
TTY: pts/0
Groups: alice,sudo,docker,...
Active sessions: 2 (192.168.1.100,203.0.113.55)
Failed 24h: 37 from 4 IP(s)
Time: 2026-04-18 19:46:12 CEST
On every failed login, a separate push fires (rate-limited to one per source-IP per 5 minutes so a brute-force flood won't spam your phone).
| File | What it does |
|---|---|
bin/ssh-login-notify.sh |
Sourced from .bashrc on SSH login. Prints the banner and sends the login push. |
bin/ssh-failed-monitor.sh |
Long-running daemon. Tails journalctl for failed SSH events and pushes them. |
bin/seclog |
CLI command — prints the same banner on demand, without sending a push. |
bin/seclog-update |
Pulls the newest commit for your checked-out branch and re-runs the installer. |
bin/seclog-restart |
Reloads systemd user units and restarts the failed-login monitor service. |
systemd/seclog-linux-fail-monitor.service |
User-level systemd unit that supervises the daemon. |
ntfy/server.yml.example |
Recommended hardened config for self-hosted ntfy. |
If you installed seclog-linux from a git checkout, two helper commands are
available after ./install.sh:
seclog-update: fetches the newest commit for the currently checked-out branch, shows your current and target commit, asks for confirmation only when an update is available, then runs the update, re-runsinstall.shand sends a push notification about the applied update.seclog-restart: reloads user systemd units and restarts the failed-login monitor service.
Example update flow:
cd ~/Projects/seclog-linux
seclog-updateTypical output:
== seclog-update ==
Repo: /home/you/Projects/seclog-linux
Branch: 1.0.1
Remote: https://github.com/arn-c0de/seclog-linux.git
Current: abc1234
Target: def5678
Message: Harden seclog-update trust boundaries
Verify: commit signature required
VERIFIED: yes
Signer: arn-c0de@protonmail.com with ED25519 key SHA256:CTFPPmCdjzltcUEfz5uJvfLrKuj6vJzveU/kfk6Gvlo
Update: available
Proceed with update? [y/N]
- Press
yorYto continue. - Any other key or an empty input aborts the update.
- If
CurrentandTargetare identical,seclog-updateexits without re-running the installer. - After a successful update,
seclog-updatesends a push with host, source IP, branch, old commit, new commit, commit text and timestamp. - The terminal output also shows the target commit text, and on an already current checkout it prints the current commit hash together with its subject.
- If signature verification is enabled, the terminal output also shows an
explicit
VERIFIED: yesline and the signer identity for the target commit. - By default,
seclog-updateonly allows the expected repo checkout at~/Projects/seclog-linuxand only iforiginmatches the official repo remote. You must opt in explicitly to use a custom checkout path.
To just restart the daemon after config changes:
seclog-restartThe project intentionally combines two different event sources into one push channel:
- Interactive SSH login: handled at shell startup via
bin/ssh-login-notify.sh - Failed SSH authentication: handled in the background via
bin/ssh-failed-monitor.sh
That gives you one consistent notification stream in ntfy:
- successful login events include session context, auth method, SSH key fingerprint and recent security summary
- failed login events include source IP, attempted username and rate-limited alerting during brute-force noise
This is useful when you want one topic, one mobile subscription and one alert history for everything related to SSH access.
- Linux with
systemd+journalctl bash,curl,awk,ss,who,getent(all part of any typical server install)- An ntfy instance — either the public
https://ntfy.shor a self-hosted one
If you already have a working ntfy topic, this is enough:
git clone git@github.com:arn-c0de/seclog-linux.git
cd seclog-linux
./install.shThen edit:
~/.config/seclog-linux/configMinimum config:
NTFY_URL="https://ntfy.sh/your-unique-topic"
NTFY_TOKEN=""Finally:
seclog
exit
ssh your-user@your-serverseclog shows the current banner locally. Reconnecting via SSH triggers the
login banner and sends the push notification.
git clone git@github.com:arn-c0de/seclog-linux.git
cd seclog-linux
./install.shThe installer will:
- Copy scripts to
~/.local/bin/ - Write a default config to
~/.config/seclog-linux/config(on first run) - Hook your
.bashrcto sourcessh-login-notify.shon SSH logins - Drop a systemd user unit at
~/.config/systemd/user/seclog-linux-fail-monitor.serviceand enable it
Then edit ~/.config/seclog-linux/config:
NTFY_URL="https://ntfy.sh/your-unique-topic"
NTFY_TOKEN="" # only if your ntfy needs authRe-login via SSH — you should see the banner and get a push.
If you installed from a git checkout and want to update later:
SECLOG_REPO_DIR="$PWD" seclog-updateIf your checkout lives at ~/Projects/seclog-linux, seclog-update works without
setting SECLOG_REPO_DIR.
seclog-update always works on the currently checked-out branch. It asks for
confirmation before applying a real update, exits immediately if the checkout
is already current, and sends an ntfy push after a successful update.
The installer creates this file on first run:
~/.config/seclog-linux/configAvailable settings:
# Full ntfy topic URL
NTFY_URL="http://YOUR_NTFY_HOST:2586/YOUR_TOPIC"
# Optional bearer token for protected ntfy instances
NTFY_TOKEN=""
# How far back the login banner should summarize failed attempts
FAIL_LOOKBACK="24 hours ago"
# Failed-login push rate-limit per source IP in seconds
FAIL_RATELIMIT_WINDOW=300
# Max seconds to spend reading journal data during interactive SSH login
LOGIN_JOURNAL_TIMEOUT=2
# Push payload detail level: full or minimal
PUSH_METADATA_LEVEL="full"
# Allow seclog-update to use a custom SECLOG_REPO_DIR
ALLOW_CUSTOM_REPO_DIR=0
# Expected origin remotes for seclog-update
EXPECTED_UPDATE_ORIGIN="https://github.com/arn-c0de/seclog-linux.git"
EXPECTED_UPDATE_ORIGIN_ALT="git@github.com:arn-c0de/seclog-linux.git"
# Require signed commits for seclog-update
VERIFY_UPDATE_SIGNATURES=0What the settings do:
NTFY_URL: Full topic endpoint including server and topic path.NTFY_TOKEN: Optional token for authenticated ntfy servers.FAIL_LOOKBACK: Human-readable window shown in the banner, for example1 hour agoor7 days ago.FAIL_RATELIMIT_WINDOW: Prevents push spam during brute-force attempts.LOGIN_JOURNAL_TIMEOUT: Caps how long interactive login waits onjournalctlbefore continuing.PUSH_METADATA_LEVEL: Set tominimalto omit UID, groups, reverse-DNS host, TTY and SSH key fingerprint from login pushes.ALLOW_CUSTOM_REPO_DIR: Keepsseclog-updatepinned to~/Projects/seclog-linuxunless you explicitly allow another checkout path.EXPECTED_UPDATE_ORIGIN/EXPECTED_UPDATE_ORIGIN_ALT:seclog-updateaborts iforigindoes not match one of these remotes.VERIFY_UPDATE_SIGNATURES: When set to1,seclog-updaterequiresgit verify-committo succeed for the target commit before applying it.
PUSH_METADATA_LEVEL changes the login push payload like this:
full: includes username, UID, sudo hint, client IP/port, reverse-DNS host, auth method, key type, SSH key fingerprint, TTY, groups, active session summary, failed-attempt summary and timestamp.minimal: keeps only username, sudo hint, client IP/port, auth method, active session summary, failed-attempt summary and timestamp.
This setting affects the interactive SSH login push. It does not change the
failed-login alert format or the update notification sent by seclog-update.
seclog-update sends a separate management push after a real update with:
- hostname of the machine that ran the update
- detected local source IP of that machine
- branch name
- previous commit
- new commit
- commit text / subject line
- timestamp
Example update push:
seclog updated on raspberrypi: Show commit text in seclog-update output
Host: raspberrypi
IP: 192.168.178.244
Branch: 1.0.1
From: 5b05d66
To: 78a6287
Commit: Show commit text in seclog-update output
Time: 2026-04-18 21:12:00 CEST
Security behavior of seclog-update:
- It changes into the repository using
cd --and resolves the canonical path first. - It runs the installer via the absolute path inside the checked-out repository.
- It refuses updates from unexpected
originremotes. - It refuses a custom
SECLOG_REPO_DIRunlessALLOW_CUSTOM_REPO_DIR=1is set. - It can optionally enforce signed commits with
VERIFY_UPDATE_SIGNATURES=1.
To use signed-update verification in practice:
- Configure your local repo to sign commits with a trusted key.
- Put the matching public key into an
allowed_signersfile on the target host. - Set
VERIFY_UPDATE_SIGNATURES=1in~/.config/seclog-linux/config. - Run
seclog-update. It will abort unlessgit verify-commitsucceeds for the target commit.
For the full trust model, threat boundaries and limits of this mechanism, see SECURITY.md.
For SSH signing, an allowed_signers line looks like this:
arn-c0de@protonmail.com ssh-ed25519 AAAA...
- Install the project with
./install.sh. - Configure
NTFY_URLand optionallyNTFY_TOKEN. - Run
seclogto check local output. - Run
seclog-restartif you changed the config while the failed-login monitor was already running. - Reconnect via SSH to verify the interactive login banner and login push.
- Trigger one intentionally failed SSH login from another machine to verify the failed-login alert path.
- Later, update the installed checkout with
seclog-update.
The failed-login monitor runs under your user systemd. By default it stops when your last session ends. For 24/7 operation, enable lingering once:
sudo loginctl enable-linger "$USER"Your push payload contains your username, client IP, SSH key fingerprint and
group membership — don't publish that to the public ntfy.sh. Use a
self-hosted ntfy inside your LAN or behind a TLS reverse proxy.
See ntfy/server.yml.example for a hardened config: auth-default-access: deny-all
plus generous home-server rate limits. Create users and tokens:
docker exec -it ntfy ntfy user add --role=admin admin
docker exec ntfy ntfy token add adminUse the resulting tk_… token as NTFY_TOKEN in the config.
Use these checks after installation:
Check that the CLI works:
seclog
seclog "1 hour ago"Check that the user service is active:
systemctl --user status seclog-linux-fail-monitorReload and restart the service after config edits:
seclog-restartCheck recent daemon logs:
journalctl --user -u seclog-linux-fail-monitor -n 50Check that the ntfy endpoint itself accepts a message:
curl -fsS -d "test from seclog-linux" "$NTFY_URL"For token-protected ntfy:
curl -fsS -H "Authorization: Bearer $NTFY_TOKEN" \
-d "test from seclog-linux" "$NTFY_URL"Then verify the two real event paths:
- Successful login: reconnect via SSH and confirm you see the banner and receive a push.
- Failed login: from another machine, run
ssh nosuchuser@YOUR_SERVERand confirm you receive a failed-login push.
| Symptom | Likely cause / fix |
|---|---|
seclog shows data, but no push arrives |
NTFY_URL wrong, NTFY_TOKEN wrong, or ntfy is unreachable. Test with curl directly. |
curl or seclog gets 403 forbidden from ntfy |
Your ntfy server requires auth and NTFY_TOKEN is missing or invalid. Put a valid tk_... token into ~/.config/seclog-linux/config, then run seclog-restart. |
| Login banner does not appear on SSH | .bashrc only runs for interactive shell sessions. Test with ssh -t host. |
| Failed-login pushes do not arrive | Check systemctl --user status seclog-linux-fail-monitor and journalctl --user -u seclog-linux-fail-monitor -n 50. |
| Login history or failed-attempt summaries stay empty | Your user may not be allowed to read system SSH logs. On affected distros, add the user to systemd-journal, then log out and back in: sudo usermod -aG systemd-journal "$USER" |
| Failed-login monitor stops after logout | Run sudo loginctl enable-linger "$USER" once. |
Public ntfy.sh works, but you are leaking too much metadata |
Use a self-hosted ntfy server. The payload includes username, client IP, group membership and SSH key fingerprint. |
| The service starts, but sees no failures | Verify that your distro logs SSH failures to journalctl for sshd or sshd-session. |
The project separates interactive login handling from background monitoring:
bin/ssh-login-notify.sh: Runs from.bashrcon interactive SSH logins.bin/ssh-failed-monitor.sh: Watches the journal continuously and pushes failed-login events.bin/seclog: Prints the security summary without sending a push.bin/seclog-update: Updates a git checkout on its current branch, asks for confirmation when needed, re-runsinstall.sh, then sends an ntfy update push with host/IP, commit change and commit text. It also validates the repo path and expectedorigin, and can optionally verify commit signatures.bin/seclog-restart: Reloads and restarts the failed-login monitor user service after config or unit changes.systemd/seclog-linux-fail-monitor.service: Keeps the failed-login monitor alive as a user service.
This means:
ssh host, opening a normal shell: banner + login push.ssh host command,scp,sftp: usually no banner, because.bashrcis not used for a normal interactive shell.- Failed SSH attempts: handled by the daemon through the journal, independent of interactive shell startup.
./uninstall.shRemoves the scripts, the systemd unit, and the .bashrc hook. Leaves your
config and state cache untouched.
- Only interactive SSH logins trigger the banner/push (
.bashrcisn't sourced forssh host cmd/scp/sftp). The failed-login daemon catches all authentication failures viajournalctl, regardless of session type. - The SSH key fingerprint in the push is a SHA256 of the public key — it cannot be used to impersonate you. It's useful as an authenticity anchor: a fingerprint you don't recognize = unknown device logging in.
- If you want lower disclosure in notifications, set
PUSH_METADATA_LEVEL="minimal"in~/.config/seclog-linux/config. - ntfy over plain HTTP within a trusted LAN is acceptable; for anything traversing the internet, put TLS in front of it.
- The daemon rate-limits pushes to one per source-IP per 5 minutes (configurable
via
FAIL_RATELIMIT_WINDOW) to survive brute-force floods without DoS-ing your phone.
If you want to report a vulnerability, do not open a public issue first.
See SECURITY.md and contact arn-c0de@protonmail.com.
MIT — see LICENSE.