Persistent external heartbeat monitoring on IPFire with Gatus

I wanted a very small external heartbeat from IPFire to a Gatus instance running outside the homelab. https://gatus.io

The requirements were:

  • outbound HTTPS only;
  • no monitoring agent listening on IPFire;
  • no dependency on the local monitoring stack;
  • heartbeat every 5 minutes, can be less;
  • external status becomes unhealthy after 10 minutes without a report;
  • custom scheduling should avoid IPFire’s system-managed root fcrontab.

The last point matters because custom entries added to root’s fcrontab can be lost when IPFire updates the fcron package or replaces the system-managed crontab.

The cleaner approach is to use the existing non-root fcronuser account for locally managed jobs.

On my system:

fcronuser:x:999:999:non root fcrontab user:/:/bin/false

and /etc/fcron.allow contains:

root
fcronuser

The heartbeat itself needs no root privileges.

1. Create the external endpoint in Gatus

Example:

external-endpoints:
  - name: Firewall Heartbeat
    group: Infrastructure
    token: YOUR_RANDOM_TOKEN
    heartbeat:
      interval: 10m

Generate a token with:

openssl rand -hex 32

With the example above, the derived endpoint key is:

infrastructure_firewall-heartbeat

and the sender URL becomes:

https://status.example.com/api/v1/endpoints/infrastructure_firewall-heartbeat/external?success=true

2. Store the endpoint configuration on IPFire

Create:

/usr/local/etc/gatus-heartbeat.conf

Contents:

GATUS_URL=https://status.example.com/api/v1/endpoints/infrastructure_firewall-heartbeat/external?success=true
GATUS_TOKEN=YOUR_RANDOM_TOKEN

The scheduled job will run as fcronuser, so give that account ownership while keeping the file private:

chown fcronuser:fcronuser /usr/local/etc/gatus-heartbeat.conf
chmod 600 /usr/local/etc/gatus-heartbeat.conf

3. Create the sender script

Create:

/usr/local/bin/gatus-heartbeat.sh

Contents:

#!/bin/sh
set -eu

. /usr/local/etc/gatus-heartbeat.conf

/usr/bin/curl \
  --fail \
  --silent \
  --show-error \
  --max-time 15 \
  -X POST \
  -H "Authorization: Bearer ${GATUS_TOKEN}" \
  "${GATUS_URL}"

Set ownership and permissions:

chown fcronuser:fcronuser /usr/local/bin/gatus-heartbeat.sh
chmod 700 /usr/local/bin/gatus-heartbeat.sh

The ownership is important.

I initially tested the script manually as root and it worked, but the scheduled job did not. The reason was simply that the script was mode 700 and the configuration mode 600 while both were still owned by root.

Test it as the actual scheduler user instead:

su -s /bin/sh -c '/usr/local/bin/gatus-heartbeat.sh' fcronuser

Successful execution is silent.

4. Add the heartbeat to fcronuser’s fcrontab

View the current custom fcrontab:

fcrontab -u fcronuser -l

Edit it:

fcrontab -u fcronuser -e

Add:

# External Gatus heartbeat
*/5 * * * * /usr/local/bin/gatus-heartbeat.sh >/dev/null 2>&1

This sends a heartbeat every five minutes.

The corresponding Gatus stale threshold is ten minutes:

heartbeat:
  interval: 10m

I deliberately use 5 minutes for the sender and 10 minutes for the stale threshold.

Using identical values such as 5m/5m can produce short false unhealthy periods because normal scheduler timing, DNS resolution, TLS negotiation and network latency can put the Gatus check just ahead of the next heartbeat.

A 5m/10m relationship allows one delayed or missed report without immediately declaring an outage.

5. Why use fcronuser instead of root?

IPFire manages root’s system fcrontab.

Custom entries added there may therefore be replaced during a Core Update when the relevant package or system scheduling configuration is updated.

Using fcronuser keeps locally managed jobs separate:

IPFire system/root fcrontab
    = managed by IPFire

fcronuser fcrontab
    = locally managed jobs

This avoids the known root-fcrontab replacement problem.

It is not an absolute guarantee that every related scheduling file will survive every future Core Update.

In particular, /etc/fcron.allow has itself been replaced during at least some updates, so I verify the custom scheduler configuration after major Core Updates.

Useful checks are:

grep '^fcronuser:' /etc/passwd

grep '^fcronuser$' /etc/fcron.allow

fcrontab -u fcronuser -l

ls -l /usr/local/bin/gatus-heartbeat.sh \
      /usr/local/etc/gatus-heartbeat.conf

The desired state is:

  • fcronuser still exists;
  • fcronuser is allowed to use fcron;
  • its fcrontab still contains the heartbeat;
  • the script and configuration still exist with the expected ownership and permissions.

6. What the heartbeat actually proves

A healthy heartbeat means IPFire was recently able to:

  • run the fcron job;
  • execute the heartbeat as fcronuser;
  • read its local configuration;
  • resolve the external hostname;
  • reach the Internet;
  • establish and validate HTTPS;
  • authenticate to the Gatus endpoint;
  • submit a successful result.

It does not prove that every firewall function, VPN, interface, DNS service or local network path is healthy.

It is deliberately a dead-man check.

If IPFire, the WAN connection, DNS, fcron, the sender script or the outbound HTTPS path stops working, Gatus stops receiving reports and marks the endpoint unhealthy after the configured timeout.

No inbound monitoring port is opened on IPFire.

7. Optional backup hardening

If the custom fcronuser schedule is important enough to restore as part of an IPFire recovery, it may also be worth ensuring the relevant fcronuser spool files under:

/var/spool/cron/

are included in the local backup strategy.

The exact backup handling should match the rest of the IPFire installation rather than assuming that custom spool content is automatically covered.

Tested environment

Linux 6.18.32-ipfire x86_64

curl 8.20.0
OpenSSL 3.6.3

/usr/bin/fcrontab
/usr/sbin/fcron
/usr/bin/curl

The important part is not Gatus specifically. The same pattern works for any small outbound HTTPS heartbeat where the custom schedule should remain separate from IPFire’s system-managed root fcrontab.

How much of this is AI?

A lot of the description steps of documentation was partly organized by AI but revised by me.

Are you saying it does not work because of that?

Looking to add additional sensors for IPFire, but doing it slowly.

I have an internal Grafana that monitors a lot, so the Gatus external solution is mostly to see that the firewall is alive, transmitting and not burning.