|
| 1 | +# zentryx-status |
| 2 | + |
| 3 | +> Public status page service for Zentryx Network. Polls every critical |
| 4 | +> endpoint, persists checks in H2, exposes a small REST API, alerts on |
| 5 | +> state changes via Discord. |
| 6 | +
|
| 7 | +Built specifically as a **production dogfood** for [`sealed-env`](https://github.com/davidalmeidac/sealed-env) |
| 8 | +in **enterprise mode** — secrets are sealed behind a TOTP-bound JWS |
| 9 | +unseal token. Every deploy requires a fresh 6-digit code from the |
| 10 | +operator's authenticator app; the token is bound to a specific commit |
| 11 | +SHA and expires in 60 seconds. |
| 12 | + |
| 13 | +If the JVM restarts (OOM, host reboot, etc.) the token is gone and |
| 14 | +the service crash-loops until a human re-runs `deploy.sh`. By design: |
| 15 | +secrets never re-decrypt without explicit human approval. |
| 16 | + |
| 17 | +## Threat model |
| 18 | + |
| 19 | +| Attacker has... | Can they decrypt secrets? | |
| 20 | +|---|---| |
| 21 | +| `.env.sealed` only | ❌ — needs master key | |
| 22 | +| Master key only | ❌ — needs current TOTP code | |
| 23 | +| TOTP secret (base32) only | ❌ — needs master key + signing key | |
| 24 | +| Stolen unseal token from CI logs | ❌ — bound to deploy_id, expired in 60s | |
| 25 | +| Master key + TOTP secret + signing key | ✅ — but that's 3 separate exfils | |
| 26 | +| Container memory dump while running | ✅ — same as any service, mitigated by host hardening | |
| 27 | + |
| 28 | +## What it monitors (out of the box) |
| 29 | + |
| 30 | +| Target | URL | Expects | |
| 31 | +|---|---|---| |
| 32 | +| `zentryxnet-web` | https://zentryxnet.lat/health | 200 | |
| 33 | +| `zentryxnet-blog` | https://zentryxnet.lat/blog | 200 | |
| 34 | +| `speedtest-ping` | https://speed.zentryxnet.lat/ping | 204 | |
| 35 | +| `mail-https` | https://mail.zentryxnet.lat/ | 200 | |
| 36 | + |
| 37 | +Edit `application.yml → zentryx.status.targets` to add/remove. Every |
| 38 | +target gets one HTTP probe every 30s. |
| 39 | + |
| 40 | +## Public API |
| 41 | + |
| 42 | +| Endpoint | Description | |
| 43 | +|---|---| |
| 44 | +| `GET /api/v1/status` | Current snapshot — last known state per target | |
| 45 | +| `GET /api/v1/uptime?days=7` | Uptime % per target over N days (1-90) | |
| 46 | +| `GET /api/v1/history?target=X&hours=24` | Raw checks for a single target | |
| 47 | + |
| 48 | +All three are public and CORS-friendly — embed them in dashboards, |
| 49 | +hit them from `curl`, scrape them with a bot. |
| 50 | + |
| 51 | +## Local development (basic mode) |
| 52 | + |
| 53 | +For day-to-day dev you don't want to type a TOTP every restart. Use |
| 54 | +basic mode locally: |
| 55 | + |
| 56 | +```bash |
| 57 | +# 1. Switch to basic mode in dev |
| 58 | +sealed-env init # generates master key in .env.local |
| 59 | +cp .env.example src/main/resources/.env |
| 60 | +# edit src/main/resources/.env with real values |
| 61 | + |
| 62 | +sealed-env encrypt src/main/resources/.env \ |
| 63 | + -o src/main/resources/.env.sealed |
| 64 | + |
| 65 | +# 2. application.yml: temporarily set sealed-env.mode=basic for dev |
| 66 | +# 3. Run |
| 67 | +mvn spring-boot:run |
| 68 | +``` |
| 69 | + |
| 70 | +Production stays in enterprise mode regardless. |
| 71 | + |
| 72 | +## Production deploy (enterprise mode) |
| 73 | + |
| 74 | +### One-time setup on `zentryx-web` |
| 75 | + |
| 76 | +```bash |
| 77 | +ssh zentryx-web |
| 78 | + |
| 79 | +# 1. Install sealed-env CLI |
| 80 | +sudo npm i -g sealed-env |
| 81 | + |
| 82 | +# 2. Clone the repo |
| 83 | +sudo mkdir -p /opt/zentryx |
| 84 | +sudo git clone https://github.com/Zentryx-Network/zentryx-status.git \ |
| 85 | + /opt/zentryx/status |
| 86 | +cd /opt/zentryx/status |
| 87 | + |
| 88 | +# 3. Drop the master key + signing key + TOTP secret |
| 89 | +# These were created by `sealed-env init --mode enterprise` |
| 90 | +# on YOUR LAPTOP — copy that .env.local here. |
| 91 | +sudo install -m 600 -o root -g root /tmp/sealed.env.local .env.local |
| 92 | +rm /tmp/sealed.env.local |
| 93 | +``` |
| 94 | + |
| 95 | +### Every deploy |
| 96 | + |
| 97 | +```bash |
| 98 | +ssh zentryx-web |
| 99 | +cd /opt/zentryx/status |
| 100 | +git pull --ff-only |
| 101 | +./deploy.sh |
| 102 | +# It asks for a 6-digit TOTP, mints a 60s token bound to the new |
| 103 | +# commit, builds Docker, starts container, tails logs, verifies |
| 104 | +# health. Total time ~25-40 seconds. |
| 105 | +``` |
| 106 | + |
| 107 | +The deploy script refuses to run on a dirty working tree — your |
| 108 | +working copy must be clean and the code that ships matches the |
| 109 | +deploy_id the token is bound to. |
| 110 | + |
| 111 | +### External monitor (mandatory) |
| 112 | + |
| 113 | +Because zentryx-status uses fail-safe crashloops on bad/expired |
| 114 | +tokens, its own AlertDispatcher can't tell you when it's down. The |
| 115 | +companion [`external-monitor/`](./external-monitor/) lives on |
| 116 | +zentryx-web (different host), runs every 60s via systemd timer, |
| 117 | +and posts to Discord after 5min of consecutive failures. |
| 118 | + |
| 119 | +The Discord webhook for the canary is **NOT** inside `.env.sealed` |
| 120 | +— it's in plaintext at `/etc/zentryx/external-monitor.env` (chmod |
| 121 | +600, root-owned). Different threat model: this webhook is the one |
| 122 | +piece that keeps working when the sealed pipeline fails. |
| 123 | + |
| 124 | +See [`external-monitor/README.md`](./external-monitor/README.md) for |
| 125 | +install instructions. |
| 126 | + |
| 127 | +## How it differs from a generic Spring Boot service |
| 128 | + |
| 129 | +- **`sealed-env-spring-boot-starter` in enterprise mode**: every |
| 130 | + deploy requires a fresh TOTP code from the operator. Token is |
| 131 | + bound to commit SHA; expires in 60 seconds. Same encrypted file |
| 132 | + format works in Node ([news-scout](https://github.com/Zentryx-Network/news-scout)) |
| 133 | + and Spring Boot. |
| 134 | +- **Strict timeouts on every outbound request**: 4s connect + 8s |
| 135 | + read. A frozen target never holds up the next polling cycle. |
| 136 | +- **Append-only schema**: `health_check` is INSERT-only, no |
| 137 | + UPDATEs. Daily prune drops anything older than 30 days. |
| 138 | +- **Cooldown on alerts**: a flapping target generates one Discord |
| 139 | + ping every 15 minutes max, not one every 30 seconds. |
| 140 | +- **Bound to 127.0.0.1**: Spring app only listens on localhost. |
| 141 | + External access goes through Caddy/nginx, which terminates TLS. |
| 142 | +- **External canary**: a separate process (different host, different |
| 143 | + webhook, no shared deps) tells you when this service goes silent. |
| 144 | + |
| 145 | +## License |
| 146 | + |
| 147 | +MIT. See [LICENSE](./LICENSE). |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +Built by [Zentryx Network](https://zentryxnet.lat). Powered by |
| 152 | +[sealed-env](https://github.com/davidalmeidac/sealed-env) (enterprise mode). |
0 commit comments