eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1<!-- 🤖 Public fleet hardening posture: per box in the mesh, whether SSH is
2 locked down (docs/status.md § The fleet hardening dashboard). Two halves,
3 both in ./hardening.json (deploy/hardening-posture.sh): the LOCAL box reads
4 its own OS state authoritatively (ufw / sshd / systemd / wireguard); each
5 PEER is seen only from an outside :22 banner-grab probe from the mesh.
6 Fully self-contained: inline CSS/JS, no external requests except that one
7 relative JSON. Unknown renders LOUD — a box whose exposure can't be read
8 never shows as a silent pass. -->
9<!doctype html>
10<html lang="en">
11<head>
12<meta charset="utf-8" />
13<meta name="viewport" content="width=device-width, initial-scale=1" />
14<meta name="color-scheme" content="light dark" />
15<title>Eleven — fleet hardening posture</title>
16<link rel="icon" type="image/png" href="./icon-192.png" />
17<link rel="stylesheet" href="./status.css" />
18<style>
19 /* Hardening-only styles — the per-box cards, the loud/calm/unknown exposure
20 chips, and the small sub-state chips. Everything shared (tokens, header,
21 nav, tiles, #error, footer) is in status.css. */
22 .boxes { list-style: none; padding: 0; }
23 .boxes li {
24 background: var(--surface); border: 1px solid var(--border);
25 border-radius: 12px; padding: 14px 16px; margin: 10px 0;
26 }
27 .boxes .top { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: baseline; }
28 .boxes .name { font-weight: 650; }
29 .boxes .region { color: var(--ink-2); font-size: 14px; }
30 .selfmark {
31 font-size: 12px; font-weight: 600; color: var(--ink-2);
32 border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px;
33 }
34 .detail { color: var(--ink-2); font-size: 14px; margin-top: 6px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
35 .probe-note { color: var(--ink-2); font-size: 13px; margin-top: 6px; }
36
37 /* The exposure chip carries the verdict in WORD + WEIGHT + ICON, never colour
38 alone. Exposed is loud (bold, bordered, --bad); unknown is a warn state
39 (bordered, --warn) so it can never read as a quiet pass; hardened is calm. */
40 .chip {
41 display: inline-block; margin-top: 10px; font-weight: 650; font-size: 14px;
42 border-radius: 8px; padding: 4px 10px; border: 1px solid var(--border);
43 }
44 .chip.exposed {
45 color: var(--bad); border: 2px solid var(--bad-mark); font-weight: 700;
46 background: var(--surface);
47 }
48 .chip.inconclusive {
49 color: var(--warn); border: 1px solid var(--warn); background: var(--redact-bg);
50 }
51 .chip.hardened { color: var(--good); border-color: var(--good); }
52
53 /* Small secondary chips for the self-reported sub-states. Same rule: the value
54 word carries the state; an unknown one reads "not reported" behind a border
55 so it is visibly not a pass. */
56 .subchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
57 .sub {
58 font-size: 12px; border-radius: 999px; padding: 1px 9px;
59 border: 1px solid var(--border); color: var(--ink-2);
60 }
61 .sub .k { color: var(--ink-2); }
62 .sub .v { font-weight: 650; }
63 .sub.good .v { color: var(--good); }
64 .sub.warn { border-color: var(--warn); }
65 .sub.warn .v { color: var(--warn); }
66 .sub.bad { border-color: var(--bad-mark); }
67 .sub.bad .v { color: var(--bad); font-weight: 700; }
68 .sub.muted .v { color: var(--ink-2); font-style: italic; }
69
70 .policy { border-left: 4px solid var(--series); background: var(--surface); border-top: 1px solid var(--border); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; margin: 14px 0; }
71 .policy summary { cursor: pointer; font-weight: 600; }
72 .policy summary:focus-visible { outline: 2px solid var(--series); outline-offset: 2px; border-radius: 4px; }
73 .policy p { margin-top: 8px; }
74 .empty { color: var(--ink-2); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
75 .tiles .value { font-variant-numeric: tabular-nums; }
76</style>
77</head>
78<body>
79<div class="wrap">
80 <header data-title="Fleet hardening posture"></header>
81 <nav class="status-nav" aria-label="Status"></nav>
82 <script src="./status-nav.js"></script>
83
84 <p class="lede">Whether SSH is locked down on every server in the fleet, on the
85 public record — the same transparency-not-tamper-proof idea as the access
86 ledger, one level down at the machines themselves. Each box answers about
87 <em>itself</em>: it reads its own firewall, SSH config, auto-updates and admin
88 tunnel. Boxes it can see are also <em>probed from the outside</em> — a
89 connect-only check of whether port 22 answers from the mesh. That outside view
90 is vantage-dependent, so it is deliberately cautious (below): only a box that
91 actually answers with an SSH banner is called exposed. This is a mirror held
92 up to the fleet, not a security control.</p>
93
94 <p class="asof">Last collected <time id="asof" datetime="">—</time></p>
95
96 <p id="error" role="alert">Couldn’t load the hardening posture. It may be mid-publish — this page retries every minute.</p>
97
98 <details class="policy">
99 <summary>How each box is judged — and what the probe can’t prove</summary>
100 <p>🤖 <strong>The local read is authoritative; the outside probe is a hint.</strong>
101 A box reports its own posture from its own OS: <em>wireguard-only</em> when
102 port 22 is allowed only over the admin tunnel (<code>wg0</code>) and there is
103 no bare public rule, <em>internet-facing</em> the moment a public port-22
104 rule exists. Those local reads are the truth for that box.</p>
105 <p>🤖 <strong>The peer <code>:22</code> probe is vantage-dependent.</strong>
106 One box connecting to another’s port 22 only sees the fleet from where it
107 stands — a firewall might admit the probing box but deny everyone else. So
108 the probe is read conservatively: only a returned <code>SSH-…</code> banner
109 is a confident <em>exposed</em>; a clean refusal or timeout is <em>not
110 reachable</em>; anything else that isn’t a clean refusal is
111 <em>inconclusive</em> — shown as a warn state, never a pass. The definitive
112 answer for any box is that box self-reading its own firewall once it runs the
113 collector.</p>
114 <p>🤖 <strong>Unknown is loud.</strong> Any signal the collector can’t read —
115 an unreadable firewall, a peer that hasn’t self-reported — renders as a
116 bordered warn chip, never as a quiet green pass. And like the access ledger,
117 this is transparency, not tamper-proof forensics: the box publishes its own
118 report, and the bucket it publishes to is versioned so after-the-fact edits
119 leave a history. No hostnames appear here — a box is named by the same
120 whimsical label the fleet page shows.</p>
121 </details>
122
123 <div class="tiles" id="totals"></div>
124
125 <section aria-labelledby="h-boxes">
126 <h2 id="h-boxes">Servers, most exposed first</h2>
127 <ul class="boxes" id="boxes"></ul>
128 <p class="empty" id="empty" hidden>No boxes reported yet — the collector
129 publishes each box’s posture on its next pass.</p>
130 </section>
131
132 <footer>
133 <p>Collected on the box every 5 minutes by
134 <code>deploy/hardening-posture.sh</code>: each box reads its own ufw / sshd /
135 systemd / wireguard state, and peers are probed <code>:22</code> from the
136 mesh. Method and honest limits:
137 <a href="https://github.com/elevenmessenger/messenger/blob/main/docs/status.md">docs/status.md</a>.</p>
138 </footer>
139</div>
140
141<script>
142(async function () {
143 "use strict";
144 const $ = (id) => document.getElementById(id);
145
146 const fmtInt = (n) => Number(n).toLocaleString();
147 const PLURAL = new Intl.PluralRules();
148 const plural = (n, one, many) => PLURAL.select(n) === "one" ? one : (many || one + "s");
149 const fmtDay = (iso) => new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
150 const fmtTime = (iso) => new Date(iso).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
151
152 // Which self-reported sub-states to show, in order, and how each value maps to
153 // a good / warn / bad / muted state. Only keys PRESENT on a box render — a
154 // probe-only peer carries just three, as "not reported"; a box that runs the
155 // collector carries all five with real values (its own row via source
156 // "local", and on every OTHER box's upload via the merged "self-reported").
157 const SUBS = [
158 ["root_login", "root login"],
159 ["password_auth", "password login"],
160 ["auto_updates", "auto-updates"],
161 ["wireguard", "wireguard"],
162 ["firewall_active", "firewall"],
163 ];
164 function subState(key, val) {
165 if (val === undefined || val === null || val === "unknown") return { cls: "muted", text: "not reported" };
166 switch (key) {
167 case "root_login":
168 if (val === "no") return { cls: "good", text: "no" };
169 if (val === "yes") return { cls: "bad", text: "yes" };
170 return { cls: "warn", text: String(val) }; // prohibit-password etc.
171 case "password_auth":
172 return val === "no" ? { cls: "good", text: "no" } : { cls: "bad", text: String(val) };
173 case "auto_updates":
174 return val === "active" ? { cls: "good", text: "active" } : { cls: "warn", text: String(val) };
175 case "wireguard":
176 return val === "up" ? { cls: "good", text: "up" } : { cls: "warn", text: String(val) };
177 case "firewall_active":
178 return val === true ? { cls: "good", text: "active" } : { cls: "bad", text: "inactive" };
179 }
180 return { cls: "muted", text: String(val) };
181 }
182
183 function exposureChip(box) {
184 const f = box.ssh_internet_facing;
185 const chip = document.createElement("span");
186 chip.className = "chip";
187 const glyph = document.createElement("span");
188 glyph.setAttribute("aria-hidden", "true");
189 let text;
190 if (f === true) { chip.classList.add("exposed"); glyph.textContent = "⚠ "; text = "SSH reachable from the internet"; }
191 else if (f === false) { chip.classList.add("hardened"); glyph.textContent = "✓ "; text = "SSH not internet-facing"; }
192 else { chip.classList.add("inconclusive"); glyph.textContent = "⚠ "; text = "SSH exposure not reported"; }
193 chip.append(glyph, document.createTextNode(text));
194 return chip;
195 }
196
197 function render(boxes) {
198 const list = $("boxes");
199 list.textContent = "";
200 $("empty").hidden = boxes.length > 0;
201
202 let exposed = 0, hardened = 0, notReported = 0;
203 for (const box of boxes) {
204 const f = box.ssh_internet_facing;
205 if (f === true) exposed++;
206 else if (f === false) hardened++;
207 else notReported++;
208
209 const li = document.createElement("li");
210
211 const top = document.createElement("div");
212 top.className = "top";
213 const name = document.createElement("span");
214 name.className = "name";
215 name.textContent = box.label || "unidentified box";
216 top.appendChild(name);
217 if (box.region) {
218 const r = document.createElement("span");
219 r.className = "region";
220 r.textContent = box.region;
221 top.appendChild(r);
222 }
223 if (box.is_self) {
224 const s = document.createElement("span");
225 s.className = "selfmark";
226 s.textContent = "this box";
227 top.appendChild(s);
228 }
229 li.appendChild(top);
230
231 li.appendChild(exposureChip(box));
232
233 if (box.detail) {
234 const d = document.createElement("div");
235 d.className = "detail";
236 d.textContent = box.detail;
237 li.appendChild(d);
238 }
239
240 const subs = document.createElement("div");
241 subs.className = "subchips";
242 for (const [key, label] of SUBS) {
243 if (!(key in box)) continue; // field this box doesn't claim
244 const st = subState(key, box[key]);
245 const chip = document.createElement("span");
246 chip.className = "sub " + st.cls;
247 const k = document.createElement("span"); k.className = "k"; k.textContent = label + ": ";
248 const v = document.createElement("span"); v.className = "v"; v.textContent = st.text;
249 chip.append(k, v);
250 subs.appendChild(chip);
251 }
252 if (subs.childNodes.length) li.appendChild(subs);
253
254 if (box.source === "probe") {
255 const note = document.createElement("p");
256 note.className = "probe-note";
257 note.textContent = "Seen from another box in the mesh — this box’s own report appears once it runs the collector.";
258 li.appendChild(note);
259 } else if (box.source === "self-reported") {
260 const note = document.createElement("p");
261 note.className = "probe-note";
262 note.textContent = "Self-reported by this box; its port 22 is also probed from the mesh outside.";
263 li.appendChild(note);
264 }
265
266 list.appendChild(li);
267 }
268
269 const total = boxes.length;
270 const tiles = [
271 [plural(total, "box", "boxes"), fmtInt(total)],
272 ["hardened", fmtInt(hardened)],
273 ["exposed", fmtInt(exposed)],
274 ["not reported", fmtInt(notReported)],
275 ];
276 const totals = $("totals");
277 totals.textContent = "";
278 for (const [label, val] of tiles) {
279 const tile = document.createElement("div");
280 tile.className = "tile";
281 const l = document.createElement("div"); l.className = "label"; l.textContent = label;
282 const v = document.createElement("div"); v.className = "value"; v.textContent = val;
283 tile.append(l, v);
284 totals.appendChild(tile);
285 }
286 }
287
288 async function load() {
289 try {
290 const r = await fetch("./hardening.json");
291 if (!r.ok) throw 0;
292 const doc = await r.json();
293 $("error").classList.remove("show");
294 const asof = $("asof");
295 asof.dateTime = doc.generated || "";
296 asof.textContent = doc.generated ? fmtDay(doc.generated) + " " + fmtTime(doc.generated) : "—";
297 render(Array.isArray(doc.boxes) ? doc.boxes : []);
298 } catch {
299 $("error").classList.add("show");
300 }
301 }
302 await load();
303 setInterval(load, 60000); // same retry cadence as the other dashboards
304})();
305</script>
306</body>
307</html>
308