#!/usr/bin/env bash # πŸ€– Public fleet hardening posture β€” the box-side collector (docs/status.md Β§ # The fleet hardening dashboard). Every 5 minutes (lchat-hardening.timer) this # publishes, per box in the mesh, whether SSH is locked down: the LOCAL box # reads its own OS state authoritatively (ufw / sshd / systemd / wireguard), # and each PEER box is probed from the outside β€” a connect-and-banner-grab :22 # check, never a login. It writes hardening.json and uploads it + hardening.html # to the public status bucket the site serves at hardening.html. Installed on # the box at exactly /opt/lchat/hardening-posture.sh (the unit's ExecStart # points there). # # ============================ WHAT THIS PUBLISHES ============================ # Everything here is PUBLIC, forever (the bucket is versioned). Two design # points make that safe, and one is the whole reason the page exists: # - NO HOSTNAMES. A box is named by its whimsical, hash-derived label # ("hypatia-whistling", the SAME label fleet.json shows) and a coarse region # ("EU Β· Finland"). A peer's real hostname is read only to point the :22 # probe at it and dies in this process β€” it never reaches the JSON, matching # the status site's no-hostnames rule (deploy/status-publish.sh's header). # - UNKNOWN IS LOUD, NEVER A GUESSED PASS. Any signal the collector can't read # is emitted as "unknown" (a null ssh_internet_facing, or an "unknown" # sub-state) and the page renders it as a warn state β€” never silently as # hardened. An unreadable firewall must look unresolved, not safe. # - THE POINT: an unhardened box surfaces publicly. If a mesh box is serving # SSH to the internet, its :22 probe reads back an "SSH-…" banner and it # sorts to the TOP of the page. Transparency, not a security control. # # ---------------------- The peer :22 probe is VANTAGE-DEPENDENT -------------- # An external probe sees the fleet from THIS box's viewpoint only: a peer's # firewall might allow this box but deny everyone else, or vice-versa. So the # probe is classified conservatively β€” only a returned SSH banner is a CONFIDENT # "exposed": # - reads back "SSH-…" -> exposed (true) [confident BAD] # - clean refusal / connect times out -> not reachable (false) [good] # - connects but no SSH banner, or anything that isn't a clean refusal # -> inconclusive (null) [UNKNOWN β€” loud warn, # neither a pass nor a confirmed fail] # The AUTHORITATIVE signal is a box self-reading its own ufw once it runs this # collector; the probe is a best-effort outside check that makes a blatantly # open box impossible to miss. The page says as much in its caveat. # ============================================================================ # # This script is DELIBERATELY STANDALONE from deploy/status-publish.sh (the # box's copy of that script has diverged from main and must not be forced to # reconcile for this feature): it keeps its own state, writes its own outputs, # and uploads them itself with the same status-publisher creds. It reads the # mesh membership from the hibernation bucket exactly as status-publish.sh's # mesh_peer_boxes does, so the two never need to share code. # # Usage: # hardening-posture.sh # what the timer runs # # Test mode (no ufw/sshd/systemd/ip, no S3, no :22 probe, no upload): point # every input at a fixture and pin "now". The command seams eval a shell command # (so a fixture is just `cat file` printing pre-captured tool output): # HARDENING_UFW_CMD='cat ufw.txt' # replaces `ufw status` # HARDENING_SSHD_CMD='cat sshd-T.txt' # replaces `sshd -T` # HARDENING_SYSTEMCTL_CMD='cat au.txt' # replaces `systemctl is-active unattended-upgrades` # HARDENING_IP_CMD='cat wg0.txt' # replaces `ip link show wg0` # HARDENING_ROUTER_ENV=router.env # LCHAT_MESH_SELF / LCHAT_BOX_REGION seams # HARDENING_MEMBERS_FILE=members.tsv # replaces the S3 listing + stats + :22 probe # HARDENING_NOW= HARDENING_NO_UPLOAD=1 \ # HARDENING_STATE_DIR=/tmp/state HARDENING_PAGES_DIR=/tmp/pages ./hardening-posture.sh # # HARDENING_MEMBERS_FILE is a TSV, one mesh member per line, four columns: #