Live Chrony NTP monitoring dashboard — clients, server tracking, sources, statistics, activity and host events, all in a single self-hosted Flask app.
ChronoGuard talks to the chronyc command line client on your Chrony server and
renders everything it reports as a live, auto-refreshing dashboard:
| Panel | Shows |
|---|---|
| Clients | Every NTP client connected to the server — packet counts, drops, intervals, last-seen, severity (OK / Warning / Critical) |
| Tracking | Server sync state: leap status, stratum, offsets, frequency drift, skew, root delay |
| Sources | Upstream NTP sources with source-state colors, reachability, last sample |
| Source Stats | Per-source measurement stats (samples, drift, jitter) |
| Activity | Online / offline / burst-resync / unknown source counts |
| Events | A live feed of host chrony events (time corrections, source changes) |
Every panel polls a single fast JSON endpoint — one round-trip per refresh, so it stays light even on modest hosts. All assets (Bootstrap, jQuery) are vendored locally: no CDN, no internet, no external services required.
You only need one thing: a Chrony server (the app runs on it, or points at it).
Clones, sets up a venv, installs deps, adds the chronyc sudo rule and starts a systemd service:
git clone https://github.com/OneByJorah/ChronoGuard.git
cd ChronoGuard
./install_chronoguard.sh # idempotent — safe to re-runDashboard: http://:5000/. Tear everything down with ./uninstall_chronoguard.sh.
git clone https://github.com/OneByJorah/ChronoGuard.git
cd ChronoGuard
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python3 chronoguard.py # or: gunicorn --bind 0.0.0.0:5000 chronoguard:appOpen http://localhost:5000.
docker compose up -d --build # mounts the host chrony socket by default
CHRONY_HOST=100.100.163.11 docker compose up -d --build # remote daemonCHRONY_HOST only needs setting when the container can't see the daemon's
socket (see .env.example).
Everything is optional and read from environment variables at startup:
| Variable | Default | Description |
|---|---|---|
FLASK_HOST |
0.0.0.0 |
Bind address |
FLASK_PORT |
5000 |
Server port |
FLASK_DEBUG |
false |
Debug mode (never in production) |
REFRESH_INTERVAL |
3000 |
Dashboard refresh interval (ms) |
CHRONYC_PATH |
auto-detect | Path to chronyc |
CHRONYC_TIMEOUT |
15 |
Seconds before a chronyc call gives up |
CHRONY_HOST / CHRONY_PORT |
(empty) / 323 |
Remote chrony daemon (Docker) |
EVENTS_LOG |
auto-detect | Path to the chrony log (falls back to journalctl) |
EVENTS_TAIL |
100 |
Number of trailing events to show |
See .env.example for a copy-paste template.
| Endpoint | Description |
|---|---|
GET / |
Dashboard UI |
GET /api/summary |
All sections in one JSON payload (what the UI polls) |
GET /api/clients |
Parsed chronyc clients rows |
GET /api/tracking |
Server sync/offset data |
GET /api/sources |
NTP sources (state, reach, samples) |
GET /api/sourcestats |
Per-source statistics |
GET /api/activity |
Source online/offline counts |
GET /api/events |
Tail of host chrony events |
GET /api/health |
Liveness check |
GET /data |
Alias for /api/clients (back-compat) |
If any underlying command fails (binary missing, daemon down, timeout) the JSON
still returns — the affected section carries an error string instead of the
dashboard crashing.
ChronoGuard tries plain chronyc first (read-only commands are allowed over the
local unix socket without root) and falls back to sudo chronyc. The installer
adds a scoped NOPASSWD rule for exactly /usr/bin/chronyc, so the service can
always monitor without ever needing an interactive password.
ChronoGuard/
├── chronoguard.py # Flask app (runnable: python3 chronoguard.py)
├── requirements.txt # runtime deps (pinned)
├── requirements-dev.txt # test/lint deps
├── install_chronoguard.sh # one-command systemd installer
├── uninstall_chronoguard.sh # full teardown
├── chronoguard.service # systemd unit
├── Dockerfile / docker-compose.yml
├── .env.example
├── templates/index.html # dashboard template
├── static/
│ ├── css/style.css
│ ├── js/app.js
│ ├── img/chronoguard-logo.svg
│ └── vendor/ # vendored Bootstrap 5 + jQuery (offline)
├── tests/ # mock chronyc + pytest suite
├── docs/assets/ # banner + screenshot
└── .github/workflows/ # CI (ruff + pytest) and CodeQL
The installer does this for you; doing it by hand:
sudo cp chronoguard.service /etc/systemd/system/
# edit User= to your account, then:
sudo systemctl daemon-reload
sudo systemctl enable --now chronoguardThe dashboard runs anywhere; point it at a remote daemon with CHRONY_HOST set,
and grant that host a socket (-S in chronyd.conf) or open UDP/323. On the
JorahOne tailnet this pairs naturally with a self-hosted Chrony server — no
public exposure required.
Contributions welcome — see CONTRIBUTING.md and CODE_OF_CONDUCT.md. Report vulnerabilities privately via SECURITY.md.
MIT License © Jhonattan L. Jimenez (OneByJorah) — see jorahone.com.
Built with 🌴 by OneByJorah
