How it works
Watch, recognize, shout.
Four stages, one async pipeline, one bounded channel. Here is what happens between someone typing
ssh and your phone buzzing.
-
sshd
Writes to the journal
Accepted publickey for …
-
watch.rs
Streams lines
journald, a rotating file, or stdin
-
sshd.rs
Parses
log line → typed event
-
notify.rs
Decides
filters, dedup, per-IP throttle
-
hook.rs
Posts
POST with retries, shared by both sinks
01 · Watch
Reading the journal without owning it
On a systemd host, watch.rs follows
journalctl on both ssh and
sshd — following the unit that does not exist costs nothing, which is
why the default works on every mainstream distribution without edits.
With kind = "file" it tails a log file and follows logrotate, so
there is nothing to restart after a rotation. kind = "stdin" reads a
pipe, which is what makes the pipeline testable.
Lines go into a bounded channel. The reader never waits on a webhook.
// main.rs — one channel, one flush tick, one stop future
let (tx, mut rx) = mpsc::channel(4096);
let src = tokio::spawn(watch::run(source, tx));
let mut tick = tokio::time::interval(Duration::from_secs(5));
tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
tokio::select! {
line = rx.recv() => match line {
Some(l) => n.line(&l).await,
None => { warn!("la source s'est arrêtée"); break }
},
_ = tick.tick() => n.tick().await,
_ = &mut stop => { info!("arrêt demandé"); break }
}
}
n.flush().await; // pending burst summaries go out before we exit 02 · Recognize & decide
Parse, then resist the urge to notify
sshd.rs turns a log line into a typed event: accepted, failed,
invalid user, closed. It is a pure function, tested against a real
auth.log fixture, so a distribution that formats a line differently is
a one-line fix with a test.
notify.rs is where the judgement lives: the event toggles, the
deduplication of sshd’s three password attempts per connection, and the per-IP burst window.
The five-second tick exists for exactly one reason — to flush a window that has expired even when no
new line has arrived.
The modules
- watch.rs
- journald, file or stdin → a channel of lines
- sshd.rs
- log line → Event
- notify.rs
- filters, deduplicates, groups, decides to send
- hook.rs
- message + POST with retries, shared by both destinations
- discord.rs
- Discord payload (embed)
- slack.rs
- Slack payload (coloured attachment)
- geo.rs
- optional geolocation
$ sudo -u gabier gabier check
config /etc/gabier/config.toml
serveur vps-prod-1
source journald (ssh, sshd)
évènements accepted=true failed=true invalid_user=true closed=false
anti-spam 1 échec(s) par IP, puis résumé toutes les 300s
geoip false
discord https://discord.com/api/webhooks/123/abcd…
joignable
Tout est prêt. 03 · Shout
One POST per destination, with retries
discord.rs and slack.rs only build a
payload; everything else — the request, the retries, the size limits, the clamping of mentions —
lives once in hook.rs. Configure both destinations and a Discord
outage never costs you the Slack message.
That shared layer is also why gabier check can probe every webhook
without publishing anything, and report all the broken ones rather than the first.
The security model
What Gabier can touch, and what it cannot, when started from the shipped unit.
Can
- Read the journal (group
systemd-journal) - Read
/etc/gabieras groupgabier - Open outbound TCP to your webhooks
Cannot
- Write anywhere but its private
/tmp - See
/home, devices, kernel tunables, cgroups - Gain privileges, run writable+executable memory, or exceed 64 MB
Trust
- The webhook URL is the only secret, and it can live in
/etc/gabier/envrather than the config. Rotate it if it leaks; nothing else is exposed.
On the Discord side allowed_mentions is locked down, so a username
appearing inside a log line can never trigger an @everyone. On Slack the
ping is only put in text when you configured one explicitly.
Under the hood
- tokio
- reqwest + rustls
- regex
- serde + toml
- clap
- tracing
- no OpenSSL
- webpki-roots
About [X] lines of Rust across seven modules. Small enough to read in one sitting, which is the point: you should be able to audit the thing that watches your server.
Convinced?
The install takes less time than reading this page did.