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.

  1. sshd

    Writes to the journal

    Accepted publickey for …

  2. watch.rs

    Streams lines

    journald, a rotating file, or stdin

  3. sshd.rs

    Parses

    log line → typed event

  4. notify.rs

    Decides

    filters, dedup, per-IP throttle

  5. 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
// 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 throttle in detail

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
shell
$ 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/gabier as group gabier
  • 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/env rather 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.