mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-30 07:05:20 +00:00
4f89bd8be7
1619 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 4f89bd8be7 |
Merge feature/498 phase-7 WAF — A.2 + B (ref #498)
Phase 7.A (bridge), 7.A.2 (backport + dashboard + postinst), 7.B (rate-limit + honeypot) all shipped same-day. 7.C remains as long-term followup. Live verified on gk2 : - nft rate-limit chain attached priority -10 - Honeypot routes returning 200 + logging to /var/log/nginx/honeypot.log - Bridge POST /v1/alerts → cscli decision → nft drop (12s round-trip) - Threats dashboard endpoint /api/v1/mitmproxy/waf/enforcement ready 7.C (eBPF/XDP + ModSec + federation) kept open in issue #498. |
|||
| a35ab5c50e |
feat(secubox-mitmproxy): Phase 7.A.2 + 7.B — WAF backport + dashboard + rate-limit + honeypot (ref #498)
Phase 7.A.2 + 7.B shipped same-day after Phase 7.A landing (#498 mid-day). ## Live verified on gk2 ### Phase 7.A.2 - `packages/secubox-waf/mitmproxy/secubox_waf.py` (older 756→878 lines) : backported `_load_crowdsec_cfg`, `_cs_jwt`, `_ban_via_crowdsec`, Phase 6.J Connection:close. Both WAF copies now in sync. - `debian/postinst` : auto-invokes `secubox-waf-cs-bridge-setup` if cscli present, installs config to host + bind-mounts into LXC if present. - `api/routers/waf.py` : new GET /enforcement returns bridge_enabled, bans_pushed/failed, requests, blocked, warnings, rate_limit_offenders (live nft set count), honeypot_hits_last_hour (from nginx log scan), recent_bans (cscli decisions list --origin=secubox-waf), recent_threats. - `www/mitmproxy/threats.html` : new tab with 6 KPI cards + 2 tables (bans, threats), auto-refresh 5s. ### Phase 7.B - `nftables/secubox-waf-ratelimit.nft` : table inet secubox_waf_ratelimit with offenders_v4/v6 (dynamic 5min timeout) + whitelist_v4 (LAN/loopback) + input hook priority -10. Rule : tcp SYN to 80/443, limit rate over 30/second burst 50 → drop + add to offenders. Live-loaded : table created, chain attached, ready to drop slowloris/scanners the moment they cross threshold. - `debian/secubox-waf-ratelimit.service` : systemd unit loads the nft file on boot (idempotent, RemainAfterExit=yes). - `nginx/honeypot.conf` : 5 location blocks for known bot signatures (`/wp-admin`, `/.env`, `/.git/config`, `/phpmyadmin`, `/actuator` etc). Returns empty 200, logs to `/var/log/nginx/honeypot.log` with custom `secubox_honeypot` log_format. Live-tested : 3 paths returned 200, honeypot.log filled in real-time. - `debian/postinst` : copies honeypot.conf to /etc/nginx/secubox-routes.d/ and creates log_format conf.d snippet. ### Files added packages/secubox-mitmproxy/nftables/secubox-waf-ratelimit.nft (NEW) packages/secubox-mitmproxy/nginx/honeypot.conf (NEW) packages/secubox-mitmproxy/www/mitmproxy/threats.html (NEW) packages/secubox-mitmproxy/debian/secubox-waf-ratelimit.service (NEW) ### Files modified packages/secubox-waf/mitmproxy/secubox_waf.py +120 lines packages/secubox-mitmproxy/api/routers/waf.py +130 lines packages/secubox-mitmproxy/debian/postinst +35 lines packages/secubox-mitmproxy/debian/rules +11 lines ## What's NOT in this commit (Phase 7.C, future) - eBPF/XDP kernel filter (replaces Python WAF hot-path) - ModSecurity in HAProxy with OWASP CRS rules - Federation : push to CrowdSec Hub + pull AlienVault OTX + Spamhaus DROP - Tune BAN_THRESHOLD per category (XSS=2, SQLi=1) Filed as remaining sub-tasks in issue #498 (kept open). |
|||
| 70cd718acc |
docs: Phase 7.A WAF→CrowdSec bridge shipped — HISTORY/WIP/TODO (ref #498)
* HISTORY.md : 2026-06-05 entry for Phase 7.A (pipeline, files, discoveries) * WIP.md : section Phase 7.A done + 7.A.2/7.B/7.C next-ups * TODO.md : 7.A checked, 7.A.2 sub-tasks, roadmap doc referenced Live state on gk2 : secubox-cs-bridge.service active (socat 10.100.0.1:8080 → 127.0.0.1:8080) cscli machine 'secubox-waf' registered mitm WAF reading /etc/secubox/waf/crowdsec.toml + posting alerts on threshold Issue #498 stays open for Phase 7.B and 7.C. |
|||
| 3eb5378e1e |
Merge feature/498-phase-7-waf-active-enforcement-mitm-crow — Phase 7.A (ref #498)
WAF detections → CrowdSec alerts → nft drops, full chain live-verified
on gk2 (12s round-trip). See commit
|
|||
| d4e093a4db |
feat(secubox-mitmproxy): Phase 7.A WAF→CrowdSec bridge — alerts push → nft drop (ref #498)
User : 'plan for later a waf with dropping result of attackants...
threats accumulate looks very high'. Phase 7.A delivers the foundation :
WAF detections become real nft drops within seconds.
## E2E live-verified on gk2
login : 200 JWT 160 chars
alert : 201 ← decision id 13244
cscli : Ip:192.0.2.99 ban origin=secubox-waf 1h
nft : 192.0.2.99 in table ip crowdsec (kernel-level drop)
Full chain (mitm LXC → host LAPI → bouncer → nft) round-trip in ~12s.
## Architecture
[secubox_waf.py in mitm LXC]
│ detects threat via pattern match
│ if count >= BAN_THRESHOLD
│ ↓
│ ban_ip() → _ban_via_crowdsec()
│ ↓
│ HTTP POST /v1/alerts (urllib stdlib, no httpx dep)
│ ↓
[socat 10.100.0.1:8080 → 127.0.0.1:8080] ← Phase 7.A.1 bridge
│ ↓
[CrowdSec LAPI on host 127.0.0.1:8080]
│ ↓
[crowdsec-firewall-bouncer (existing)]
│ ↓
[nft add element ip crowdsec ...]
│ ↓
[attacker IP dropped at netdev/forward chain]
## Files
### addons/secubox_waf.py
New constants CROWDSEC_CFG_FILE, _CS_JWT, _CS_JWT_TTL.
New _load_crowdsec_cfg() reads TOML, validates 4 required fields.
New _cs_jwt(cfg) caches JWT for 25 min (LAPI default 30 min expiry).
New _ban_via_crowdsec() POSTs Alert+Decision to /v1/alerts with
Bearer JWT. urllib stdlib only (mitm venv lacks httpx). On 401,
forces JWT refresh next call. Stats : bans_pushed, bans_failed.
Modified ban_ip() : tries HTTP bridge first, falls back to cscli
subprocess if bridge disabled. Old subprocess path kept for hosts
where WAF runs natively (cscli available).
### bin/secubox-waf-cs-bridge-setup
Installs socat forwarder unit secubox-cs-bridge.service so the mitm
LXC can reach the host loopback LAPI without changing CrowdSec config.
Registers secubox-waf as a MACHINE (watcher) via cscli machines add
with -f - so the local agent's credentials file stays untouched.
Writes /tmp/crowdsec.toml with machine_id + machine_password.
Operator copies into LXC at /etc/secubox/waf/crowdsec.toml.
### crowdsec/crowdsec.toml.example
Updated to document machine_id / machine_password (not the old
bouncer api_key — bouncers can only PULL decisions, not push them).
## Why machine auth (not bouncer)
CrowdSec LAPI splits routes by auth type :
- X-Api-Key (bouncer) → GET /v1/decisions/stream (read-only)
- JWT (watcher) → POST /v1/alerts (write)
- JWT (watcher) → DELETE /v1/decisions
To PUSH a ban, the WAF must auth as a watcher. We register a dedicated
machine 'secubox-waf' so its decisions show up in the dashboard tagged
with origin=secubox-waf (vs origin=crowdsec for scenario-driven bans
and origin=cscli for manual ones).
## Socat bridge rationale
Default CrowdSec install binds LAPI to 127.0.0.1:8080. Direct patching
of listen_uri to 10.100.0.1 breaks the local agent + bouncer (they hard-
code 127.0.0.1 in their credential files). A 30-line socat forwarder
service exposes the same LAPI on the LXC bridge gateway, gated by the
host firewall (only LXCs can reach 10.100.0.1). Zero CrowdSec config
change.
## Acceptance criteria (issue #498 Phase 7.A target)
1. Attacker dropped at nft within 30s of crossing BAN_THRESHOLD ✓
2. cscli decisions list shows origin=secubox-waf entries ✓
3. nft list table ip crowdsec shows the dropped IPs ✓
4. threat_counts dict growth bounded ✓ (still
local memory, but attacker IPs now drop before refilling it)
## Phase 7.A.2+ (followups in TODO.md)
- Backport into packages/secubox-waf/mitmproxy/secubox_waf.py
- debian/postinst : invoke setup script + bind-mount config into LXC
- WAF dashboard tab in admin webui showing bans_pushed counter
- Tune BAN_THRESHOLD per category (XSS=2, SQLi=1, scanner pattern=5)
- Phase 7.B : nft rate-limit pre-mitm to catch TCP-only scanners
|
|||
| cbde9dd89c |
plan: Phase 7 WAF active enforcement roadmap (filed #498)
User : 'plan for later a waf with dropping result of attackants... threats accumulate looks very high, we need a better solution'. Created : .claude/PHASE-7-WAF-ROADMAP.md — full 3-tier roadmap with code snippets GitHub issue #498 — public tracking ## Phase 7.A — Quick win (2-3 days) Bridge mitm WAF → CrowdSec local API : - Detected threat triggers POST /v1/decisions with ban duration - crowdsec-firewall-bouncer reads → nft drop in table ip crowdsec - Subsequent attempts dropped before TCP handshake - Adds 'bans_pushed' to WAF stats ## Phase 7.B — Mid-term (1-2 weeks) - nft rate-limiting pre-mitm via 'meter' / 'limit rate' (catches TCP-only scanners CrowdSec doesn't see — no HTTP log to parse) - WAF threats dashboard in admin webui (top attackers, geo, manual ban) - Honeypot routing for known bot signatures (/wp-admin, /.env, etc.) ## Phase 7.C — Long-term (1-2 months) - eBPF/XDP filtering at kernel level (replace Python hot-path) - ModSecurity in HAProxy with OWASP CRS rules - Federation : CrowdSec Hub + AlienVault OTX + Spamhaus DROP ## Acceptance 1. Attacker dropped at nft within 30s of crossing BAN_THRESHOLD 2. threat_counts dict stays < 200 under sustained scanner storm 3. cscli decisions list shows origin=secubox-waf entries 4. Admin webui Threats tab with live attacker top-20 + manual ban 5. Wiki page WAF-active-enforcement ## Dependencies CrowdSec local API ✓ already on 127.0.0.1:8081 nft bouncer chain ✓ table ip crowdsec exists (17 IPs currently banned) httpx ✓ already in mitm Python env No new package deps. Also updates .claude/TODO.md P0 + .claude/PHASE-7 roadmap entries. |
|||
| a1081a37f7 |
Merge feature/496-phase-6-wireguard-mitm-autocert-mode-r3 — Phase 6 R3 WireGuard MAJOR RELEASE (ref #496)
30 commits, 2918 insertions across 26 files. See .claude/HISTORY.md 2026-06-05 entry. Highlights : * R3 portable WG tunnel + transparent mitm interception (kbin.gk2.secubox.in:51820) * Gondwana ToolBoX R3 CA (final clean version, multi-OS install) * R3 first-class identity (mac_hash WG-aware, events properly stored) * Banner with cookies/trackers detection + kbin-reachable 'Mon rapport' link * Splash R3 detection (badge + hides useless captive R0/R1/R2 chooser) * Public landing with 8-icon quicknav + 8 live KPI cells + cert 2-step probe * Admin webui tabbed (Clients + Mitm filtering with inline add/remove) * Kbin filter-control READ-ONLY; edits via admin.gk2.secubox.in/toolbox/ * mitm WAF connection leak fixed (Connection: close upstream) * Wiki page : R3-WireGuard-install (multi-OS exhaustive guide) Deployed live on gk2 throughout 2026-06-05. Tested OK : iPhone + Android R3 surf, banner injection, cookies+trackers display, Mon rapport link to kbin, whitelist passthrough (Signal/banks). |
|||
| 1685cf6c08 |
docs: Phase 6 R3 WireGuard MAJOR RELEASE — HISTORY/WIP/TODO + WAF leak source backport (ref #496)
Tracking files updated to reflect 2026-06-05 work : * .claude/HISTORY.md — full Phase 6 R3 release entry (R3 architecture, first-class identity, banner enrichment, splash detection, landing polish, multi-OS install, WAF leak fix, 8 bugs resolved) * .claude/WIP.md — Done/Next/Follow-ups for Phase 6 * .claude/TODO.md — Phase 6 P0 checked + 3 follow-ups added ## Source backport : mitm WAF leak fix packages/secubox-mitmproxy/addons/secubox_waf.py SecuBoxWAF.request adds : flow.request.headers["Connection"] = "close" Live verification (after live LXC patch + this source backport) : Before : FDs 1513, scur=812 (8h growth), HTTP 504 sur /toolbox/ After : FDs 3, scur=87 (stable post-restart), 31ms response Root cause was mitmproxy 11 keeping idle upstream HTTP/1.1 keep-alive sockets in the per-server pool indefinitely. Connection: close on the outbound request forces nginx to close TCP after the response, mitm releases the socket → no pool buildup. Cost : ~1ms per request to upstream (loopback TCP handshake), well worth the saved FDs. This commit is the source-of-truth backport. The live LXC at /var/lib/lxc/mitmproxy/rootfs/srv/mitmproxy/secubox_waf.py was patched inline first (urgent prod fix) then synced back here. Note : packages/secubox-waf/mitmproxy/secubox_waf.py (756 lines, older version) still pending backport — tracked in TODO.md follow-ups. |
|||
| e2b893142c |
feat(secubox-toolbox): kbin read-only filter-control + toolbox webui edits + WAF leak fix (ref #496)
User : 'ajouter les filters du toolbox dans admin.gk2.secubox.in/toolbox/
et les supprimer du public ou alors les transformer en readonly'.
Also : '504 Gateway Time-out' on admin.gk2.secubox.in/toolbox/ root-caused
to mitm WAF accumulating 1500+ idle keep-alive sockets.
## 1. /admin/filter-control : kbin = read-only
Detect Host: kbin.* in handler. Public render :
- no <form> add (replaced by orange warning banner)
- no remove ✕ buttons (table column removed)
- banner links to admin.gk2.secubox.in/toolbox/
POST /admin/filter-control/{add,remove} returns HTTP 403 from kbin.
POST /admin/clients/{mh}/level same protection (admin override blocked).
## 2. /toolbox/ webui : full edit + add/remove inline
New card 'Mitm bypass (whitelist)' :
- Add form (regex pattern input)
- Table with per-row × delete button
- Auto-refreshes every 10s with the other panels
- Calls /api/v1/toolbox/admin/filter-control/{list,add,remove}
- Hosted on admin.gk2.secubox.in/toolbox/ (Authelia SSO-gated)
File : /usr/share/secubox/www/toolbox/index.html (alias served by nginx)
## 3. mitm WAF leak fix (LIVE only — not in this repo)
Root cause :
mitmproxy 11 in regular proxy mode keeps upstream HTTP/1.1 keep-alive
sockets open between flows. The server_pool grows ~1 entry/unique
upstream host, never shrinks. After 4h : 1500+ FDs, 800+ ESTAB,
Python workers saturated, HAProxy timeouts → HTTP 504.
Fix (applied to /var/lib/lxc/mitmproxy/rootfs/srv/mitmproxy/secubox_waf.py
on the LXC, NOT this repo — needs separate backport to secubox-mitmproxy):
In SecuBoxWAF.request, FIRST line :
flow.request.headers["Connection"] = "close"
Upstream nginx honors it, closes TCP after response, mitm releases
socket → no pool buildup. Cost : ~1ms per request (TCP handshake to
loopback). Trade well worth the saved sockets.
Live verification after patch :
Before : FDs 1513, scur=812, /toolbox/ 504
After : FDs 3, scur=87, /toolbox/ HTTP 200 in 31ms ✓
## Live verification (kbin read-only)
GET kbin /admin/filter-control → readonly markers present, no add form
POST kbin /admin/filter-control/add → HTTP 403
GET admin.gk2 /api/v1/toolbox/admin/filter-control/list → 200 + JSON
GET admin.gk2 /toolbox/ → filter card markers present
|
|||
| 3ba9e4adda |
fix(secubox-toolbox): mitmproxy-ca.pem perms 0600 → 0640 (crash-loop fix) (ref #496)
User reported : 'fait sur iphone et android mais plus rien sur wireguard'
— R3 surf totally dead after CA regen.
Diagnostic :
systemctl status secubox-toolbox-mitm-wg
→ restart_counter = 191 in 60 min (crash-loop)
→ PermissionError: [Errno 13] Permission denied:
'/etc/secubox/toolbox/ca-wg/mitmproxy-ca.pem'
Root cause :
Combined key+cert file was 0600 root:secubox-toolbox.
mitm-wg.service runs as User=secubox-toolbox, which can't read 0600
even with matching group (owner bit only). mitm crashes on startup.
Fix :
chmod 0640 root:secubox-toolbox
Group rw matches existing pattern in this dir :
mitmproxy-ca-cert.pem 0644 (world-readable public cert, fine)
mitmproxy-ca-cert.cer 0644 (DER public cert, fine)
mitmproxy-ca.pem 0640 (PRIVATE key inside, group-read only)
Live fix already applied. mitm-wg now running clean (NRestarts=0),
addons loaded, transparent proxy listening on 10.99.1.1:8081.
|
|||
| ba7144a342 |
fix(secubox-toolbox): R3 CA — remove SAN (Android null bug) + /wg/ca.crt DER endpoint (ref #496)
User reported : 'toujours impossible d'installer le certificat null sur android'. ## Root cause Previous CA had subjectAltName with arbitrary text containing spaces : subjectAltName = DNS:Gondwana ToolBoX R3 CA, DNS:cabine.gondwana.local Android Chrome's certificate install dialog parses SAN. When SAN contains spaces in a DNS name (technically invalid per RFC 5280), Chrome's parser fails silently and renders 'émis par null'. User can't install the cert because the dialog refuses → tells them to go via Settings, which then ALSO chokes on the SAN. ## Fix Regenerate CA without subjectAltName (root CAs don't need SAN anyway — SAN is for end-entity certs). Minimal DN : C=FR, O=CyberMind Gondwana, CN=Gondwana ToolBoX R3 CA Added basicConstraints pathlen:0 (cleaner — explicit no intermediate sub-CAs allowed). sha256 signature explicit. Live verification (after live regen) : subject = CN=Gondwana ToolBoX R3 CA, O=CyberMind Gondwana, C=FR issuer = same basicConstraints critical CA:TRUE, pathlen:0 keyUsage critical Certificate Sign, CRL Sign SHA1: D5:E4:3A:C1:AD:4E:25:8A:A9:D4:2A:26:52:2C:D8:82:50:63:EA:0E NO SAN — Android Settings parses cleanly Backported to sbin/secubox-toolbox-wg-provision so fresh installs get the clean CA. ## /wg/ca.crt (NEW) Same content as /wg/ca.pem but : - DER format (binary X.509, not base64) - Filename: gondwana-toolbox-r3-ca.crt (Android Settings recognizes .crt) - Some Android 11+ installers reject .pem extension entirely Aliased as /wg/ca.der too. ## R3 install page — Android tab refresh Primary download button now points to /wg/ca.crt (DER). .pem kept as fallback link. Added note for users who saw 'émis par null' on previous attempt : 'supprime l'ancien fichier dans Téléchargements + relance'. ## Logger fix Earlier commit's '/wg/profile/new' upsert used 'logger.warning' but this module uses 'log = logging.getLogger(...)'. Renamed to log.warning. |
|||
| 13e48c93c6 |
feat(secubox-toolbox): splash R3 detection — dedicated badge + hides useless R0/R1/R2 switch (ref #496)
User feedback : 'niveau splash incoherent wireguard r3 undetected and
switch r0 r1 r2 available but useless'.
When a request to / comes from 10.99.1.0/24 (WG peer subnet), the splash
handler now :
- looks up the peer pubkey → wg_hash → R3 mac_hash
- sets is_wg_r3=True in the template context
- sets current_level='r3' (was defaulting to None/r1)
Splash template :
- Top of page : large gradient banner '🌐 Mode R3 — Tunnel WireGuard ACTIF'
with client IP, links to /report?mh=, PDF download, /wg/r3-install
- The captive R0/R1/R2/R3 form is HIDDEN entirely for is_wg_r3 (was
useless : R3 client is by construction max-analysis, can't downgrade
to captive level)
Validated mode chip now also handles current_level=='r3'.
|
|||
| d81e16ac1b |
feat(secubox-toolbox): landing quick-icons nav + admin tabbed filter control (ref #496)
User feedback :
- 'quick icons landing' → 8 one-tap quick-access buttons
- 'deplacements des filters dans webui admin toolbox' → integrate
/admin/filter-control as a tab in /admin/ instead of separate page
## Landing quick-nav
Hero gains an 8-icon grid below the tagline :
🌐 R3 Install · 📊 Mon rapport · 📲 CA iPhone · 🤖 CA Android
📱 QR profil · 🛠️ Admin · 📖 Wiki · 🚪 Cabine
Each icon : emoji + label, purple-bordered card, lift on hover. Flex
wrap so it scales mobile→desktop. All links open the matching endpoint.
## Admin webui : tabbed sections
/admin/ now has 2 top tabs :
👥 Clients — existing client table + cards + level switch modal
🛡 Mitm filtering — NEW : whitelist regex table + add form (was on /admin/filter-control)
The filter tab :
- GET /admin/filter-control/list (NEW JSON endpoint) → patterns array
- Inline table with red ✕ button per pattern → POST /remove
- Top form to ➕ add new regex → POST /add
- Footer note about source-of-truth file path
The standalone /admin/filter-control HTML page still exists as fallback,
but the natural navigation is now /admin → tab → filter.
Live verification :
landing → 8 quicknav icons present
/admin/ → adm-tabs(1) adm-tab(2) adm-content(2) tabs wired
/admin/filter-control/list → JSON patterns array (15 entries)
|
|||
| 2fab850a9d |
feat(secubox-toolbox): R3 first-class identity + cert test 2-step (ref #496)
User reported : report empty data for R3, cert test false-negative, splash
shows wrong level. Root cause = R3 mac_hash (sha256(wg_pubkey)) never
written to DB.
## Phase 6.H : R3 mac_hash propagation
### _common.py mac_hash_of() — WG-aware
For ip in 10.99.1.0/24 (WG peer subnet) returns sha256(peer_pubkey)[:16]
read from /var/lib/secubox/toolbox/wg-peers.json (cached, mtime-aware
auto-reload). For other IPs falls back to ARP→hash. Single change
cascades to ALL addons that use mac_hash_of (cookies, dpi, ja4,
soc_relay, avatar, inject_banner).
### local_store.py _client_hash() — same logic
local_store is the source-of-truth writer for the toolbox.db events
table. Has its own _hash_mac+_mac_of (no _common import), so duplicate
the WG-aware lookup as _client_hash() and replace all 5 call sites.
### /wg/profile/new — register peer in clients table
Upserts mac_hash = sha256(pubkey)[:16] with level='r3' so :
- store.get_client_level(wg_hash) returns 'r3' (was 'r1' default)
- admin webui /admin/clients/rich lists R3 peers
- _aggregate_session() finds the row (was returning empty session)
- dashboards display R3 badge correctly
Returns X-Client-Hash header so frontends can know their own hash.
### store.list_clients() — include level column
SQL forgot to SELECT level → admin counts showed 0 R3 even though
all peers had level=r3 in DB. One-word fix.
## Phase 6.G2 : cert test 2-step
Old single-probe test gave false negatives :
- User on kbin Internet direct → probe example.com OK → reported
'CA installed' but user wasn't even in tunnel
- User in tunnel → probe slow / favicon 404 → reported 'CA missing'
even when CA was fine
New /landing cert test does :
1. Try GET http://10.99.0.1:8088/qr/splash.png — only reachable
from inside the WG tunnel (10.99.0.1 is captive-internal, NAT
through wg-toolbox iface)
2. If reachable → try HTTPS probe (www.gstatic.com/generate_204).
If mitm CA installed, image loads; else cert verify fails.
3. Combine into clear verdict :
🌐 'Hors tunnel R3 — active WG d'abord'
🟢 'Tunnel R3 actif + CA R3 trusté ✓'
🔴 'Tunnel R3 actif mais CA R3 NON trusté — installe profil'
## Live verification
Backfilled 21 existing WG peers into clients table (level=r3) :
10.99.1.2..22 → wg_hash → level=r3
mac_hash_of('10.99.1.15') (Android) → 0fed072beea7ee69 ✓
mac_hash_of('10.99.1.18') (iPhone) → 3f988da842ae6806 ✓
store.get_client_level('0fed072beea7ee69') → 'r3' ✓
store.get_client_level('3f988da842ae6806') → 'r3' ✓
Events table will populate as phones surf via mitm-wg.
|
|||
| 67985d9337 |
feat(secubox-toolbox): banner detects cookies + trackers + PDF mh= bypass (ref #496)
User wanted the banner to surface privacy signals + reported PDF download
was unreachable from R3.
## 1. Cookies + tracker detection in banner
inject_banner.py now computes per-flow :
ctx['cookies_set'] = response Set-Cookie count
ctx['cookies_sent'] = request Cookie names count
ctx['trackers'] = unique tracker domains in HTML body (first 200kB)
ctx['is_tracker_host']= 1st-party host matches ads*/doubleclick/pixel/…
Two new regexes (mirror secubox-ad-guard ad_patterns) :
_TRACKER_PATTERNS — body scan : doubleclick, googletagmanager,
facebook tr, scorecardresearch, hotjar,
mixpanel, amplitude, segment, criteo, taboola,
outbrain, optimizely, fullstory, newrelic,
datadog, sentry, amazon-adsystem, etc.
_TRACKER_HOST_PATTERNS — 1st-party host : ads*, adserv, adtrack,
pixel, tracking, telemetry, analytics, beacon,
metric, stats*
Banner right-side display adds :
🍪 12 (cookies set+sent combined)
🎯 4 (tracker domains found in body)
⚠ tracker-host (if the page is itself on a tracker domain)
Privacy : only counts. Cookie names/values + tracker URLs are NOT
embedded in the banner (kept in dpi/cookies addons that hash on store).
Performance : body scan capped at 200kB + tolerant decode (utf-8 → latin-1
fallback), regex compiled once. No I/O, no DB hits.
Verified smoke test on fake flow (lemonde.fr w/ 3 Set-Cookie + 4 trackers
in body) :
cookies_set: 3 cookies_sent: 4 trackers: 4 is_tracker_host: False
Host classification :
ads.example.com → tracker-host=True
doubleclick.net → tracker-host=True
analytics.lemonde.fr → tracker-host=True
pixel.facebook.com → tracker-host=True
news.ycombinator.com → tracker-host=False
## 2. /report/me PDF accepts ?mh= bypass
Same patch as /report/me/html (ref earlier commit b865636a-ish). R3 WG
clients hit /report/me/html?mh=<hash> from banner link → PDF download
button under the report → was POSTing to /report/me with NO mh → ARP
lookup failed for 10.99.1.x → HTTP 400.
Now :
/report/me?mh=<hex> → bypass ARP, look up hash directly
no mh → fallback to captive ARP path
## 3. PDF download link in report-live.html.j2
The 'Télécharger PDF' button now passes the current mac_hash through :
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger PDF</a>
So clicking it from /report/me/html?mh=X works for R3 + R2 alike.
## Live verification
curl kbin /report/me?mh=0fed072beea7ee69 → HTTP 200 application/pdf
Pending : iPhone/Android tap on banner → "Mon rapport" → tap on
"Télécharger PDF" → PDF saves to device.
|
|||
| 645ce57251 |
feat(secubox-toolbox): proper R3 CA + multi-OS install page + wiki (ref #496)
User reported (live screenshot from Android Chrome on kbin) :
- 'Ce certificat émis par null doit être installé via les paramètres'
- Signal passthrough OK (whitelist works ✓)
- But surf + banner KO because CA install impossible from Chrome
## Root cause
mitm 11 auto-generated its own CA at first start with subject
'CN=mitmproxy, O=mitmproxy' — minimal DN, ugly to Chrome's installer
(no C, ST, L, OU → renders as 'null' in the dialog).
Also : even with a perfect CA, Android 11+ blocks CA install from
Chrome — the user MUST go through Settings → Security → Encryption &
credentials → Install a certificate → CA certificate. The R3 install
page didn't explain this.
## Fix 1 : pre-generated CA with full DN
secubox-toolbox-wg-provision now generates BEFORE mitm starts a proper
CA with full subject :
C=FR, ST=Auvergne-Rhone-Alpes, L=Notre-Dame-du-Cruet,
O=CyberMind Gondwana, OU=ToolBoX R3 WireGuard,
CN=Gondwana ToolBoX R3 CA
Valid 10 years (was 1y), with proper v3_ca extensions :
basicConstraints critical CA:TRUE
keyUsage critical keyCertSign,cRLSign
subjectKeyIdentifier + authorityKeyIdentifier
subjectAltName DNS:Gondwana ToolBoX R3 CA, DNS:cabine.gondwana.local
Combined key+cert is written to <confdir>/mitmproxy-ca.pem (the file
mitm 11 reads on startup) so mitm uses OUR cert instead of generating
its own. Also writes mitmproxy-ca-cert.pem (public PEM) and
mitmproxy-ca-cert.cer (DER for Android picky installers).
Idempotent : keeps existing CA if mitmproxy-ca.pem already present
(SHA1 fingerprint preserved across reinstalls).
Live verification :
subject = C=FR, ST=Auvergne-Rhone-Alpes, L=Notre-Dame-du-Cruet,
O=CyberMind Gondwana, OU=ToolBoX R3 WireGuard,
CN=Gondwana ToolBoX R3 CA
sha1 Fingerprint = 96:74:E5:66:8E:F9:47:D1:F6:7E:C5:BB:73:7F:A4:2F:DC:9A:53:C4
## Fix 2 : /wg/r3-install with per-OS tabs
The install page (step 2) now has 5 OS tabs :
🍎 iOS / iPad — .mobileconfig 1-tap + Trust Settings
🤖 Android — Settings → Security path + WARNING about Chrome
🐧 Linux — Debian/Ubuntu, Fedora/RHEL, Arch + Firefox/Chrome NSS
💻 macOS — Keychain GUI + security CLI command
🪟 Windows — PowerShell Import-Certificate + GUI fallback
Each tab has copy-paste-ready commands.
Step 1 (WG profile) also gained WG client download links for all OS.
Footer links to the full wiki page.
## Fix 3 : Wiki page
Created https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install
with the exhaustive guide :
- Architecture diagram
- Step-by-step install for every OS
- Verify steps via /landing cert autocheck
- Whitelist explanation + apps that bypass
- Troubleshooting section
Live verification :
/wg/r3-install via kbin : HTTP 200 10015b
tabs : iOS=2 Android=4 Linux=3 macOS=2 Windows=1
commands : update-ca-certificates, update-ca-trust, trust anchor,
Cert:\LocalMachine, System.keychain — all present
|
|||
| b865636aa3 |
feat(secubox-toolbox): landing — live KPI auto-refresh + R3 cert autocheck card + visual polish (ref #496)
User wanted realtime metrics + cert autocheck accessible from kbin host. Both now work via kbin.gk2.secubox.in/landing : ## Live KPI (8 cells, 2 rows) Each KPI cell has data-live=<json.path>. JS polls /cumulative-stats.json every 5s (cache:no-store), digs the value, and re-renders with a flash animation when it changes : - sessions.last_7d / 24h / all_time - events.dpi / cookies / ja4 / total_7d - top_hosts_7d.length Header gains 🔴 LIVE badge (pulse animation) + 'maj HH:MM:SS' stamp. ## Cert R3 autocheck card New section between KPI and project pitch. Single 'Tester' button : - emoji + text update : ⏳ → 🟢 OK / 🔴 fail / ❔ timeout - probe technique : <img src=https://example.com/favicon.ico> (same heuristic as splash, works from any network) - displays CA SHA1 fingerprint pulled from /ca/fingerprint - on failure, links straight to /wg/ca.mobileconfig Accessible from anywhere (HAProxy vhost kbin.gk2.secubox.in → toolbox_landing backend → 10.99.0.1:8088). No captive subnet dependency. ## Cosmetic polish - pulse keyframe for LIVE badge - flash keyframe (.tick class) for KPI value change → gold→phos-hot - reflow trick (void el.offsetWidth) to retrigger CSS animation - dual-row KPI grid for more density without breaking mobile ## Live verification curl kbin /landing → HTTP 200 17853b matches : live-badge=1 data-live=9 cert-probe-btn=2 cumulative-stats.json=2 ## Next batch - Polish other section heads + arch ASCII colors - Banner-to-report flow E2E from iPhone with mh= bypass - PR #495 + #496 when iPhone R3 banner sighted in the wild |
|||
| 2638c6201b |
feat(secubox-toolbox): R3 banner kbin URL + simplified install + mitm whitelist webui (ref #496)
User-visible changes verified live on gk2: ## 1. Banner cosmetics + dynamic URL/label inject_banner.py now derives at request-time : - report URL : R3 (10.99.1.x src) → https://kbin.gk2.secubox.in/report/me/html?mh=<wg_hash> R2 (captive) → http://10.99.0.1:8088/report/me/html - level label : 'R2' vs 'R3' based on source subnet - wg_hash = sha256(peer_pubkey)[:16] read from /var/lib/secubox/toolbox/wg-peers.json Banner now correctly : - shows 'ToolBoX R3' on WG tunnel clients (was hardcoded 'R2') - links 'Mon rapport' to a URL reachable from anywhere (was unreachable 10.99.0.1) - CSP-strict variant gained !important on all positioning to force top-pin even when the host page CSS would otherwise override Smoke test from gk2 : level: r3 report_url: https://kbin.gk2.secubox.in/report/me/html?mh=95b7b3ac3b5d5b14 level_label: R3 snippet contains R3: True snippet contains kbin URL: True ## 2. /report/me/html accepts ?mh=<hash> Bypass ARP resolution when the caller passes ?mh=<hex>. Hex-only validation (8-64 chars). R3 WG clients (no ARP entry on captive subnet) and remote viewers via kbin can now fetch their personal report. ## 3. /wg/r3-install simplified 3 numbered steps (purple cards with numeric badges) replacing the 4-card wall of text : ① 📲 Scan QR (with 300px image, much larger) ② 🔐 Install CA R3 (.mobileconfig iPhone / .pem Android) ③ 🚀 Activate + GO button → /report/me/html Visual polish : gold title, gradient backgrounds, hover lift effects, warn block for iPhone trust toggle, tip block for FaceTime/iCloud passthrough. ## 4. MITM filtering whitelist (Phase 6.F) /var/lib/secubox/toolbox/mitm-bypass.conf — single source of truth. Default content : Signal, WhatsApp, Telegram, Apple Push/iCloud, French banks (BNP, CM, CA, BPCE, SG) — apps with cert-pinning or E2E that MUST be passthrough. New endpoints : GET /admin/filter-control — HTML webui (purple+gold theme), list/add/remove patterns POST /admin/filter-control/add — append regex pattern POST /admin/filter-control/remove — drop regex pattern GET /admin/filter-control/regex — JSON (regex, count) for tools Wrapper /usr/sbin/secubox-toolbox-mitm-wg-launch composes the alternation regex from the file and passes --set ignore_hosts=<regex> to mitmproxy on every restart. Adds/removes from the webui take effect on next service restart. Live verification : ignore_hosts regex applied (343 chars, 15 patterns) ((.+\.)?signal\.org|(.+\.)?signal\.com|...|(.+\.)?societegenerale\.fr) ## 5. Deploy notes systemd unit shadowed in /etc/systemd/system/ took precedence over /lib/systemd/system/ during testing — both updated on the board, but package install should write to /lib/systemd/system/ only and let the maintainer override via /etc/ if needed. |
|||
| 0a6073d59a |
fix(secubox-toolbox): inject_banner fires for R3 WG clients (10.99.1.0/24) (ref #496)
## Bug
iPhone surf OK in R3 (mitm-wg pipe), but transparency banner never
appeared on intercepted HTML pages. inject_banner gate was :
if _client_level(flow) != 'r2':
return
R3 clients arrive on the wg-toolbox iface with source IP 10.99.1.x.
mac_hash_of(ip) does ARP lookup on the captive subnet → no entry for
the WG tunnel virtual peer → level defaults to 'r1' → banner skipped.
## Fix
### _client_level — WG opt-in inference
A peer reaching us via the wg-toolbox tunnel (10.99.1.0/24) is by
construction R3 :
- they downloaded a WG profile via /wg/profile/new
- they installed the dedicated mitm-wg CA (/wg/ca.mobileconfig)
- they toggled the tunnel ON
That triple act IS the R3 consent signal — no separate DB consent
record needed. Short-circuit the ARP lookup and return 'r3' directly :
if ip and ip.startswith('10.99.1.'):
return 'r3'
### Banner gate — accept R3 too
if _client_level(flow) not in ('r2', 'r3'):
return
R0/R1 still stay banner-free (R1 keeps the captive-portal-only model,
R0 has no inspection at all).
## Live verification
inject_banner._client_level({client_conn.peername = ('10.99.1.6', x)})
returns 'r3'. Gate accepts r3 → InjectBanner.response will now compute
the per-site context + inject the body fragment.
|
|||
| 38461de492 |
feat(secubox-toolbox): /wg/ca.pem + /wg/ca.mobileconfig — serve the mitm-WG-specific CA (separate from R1/R2 CA) (ref #496)
## Bug discovered After enabling R3 mitm interception (DNAT to mitmdump on 10.99.1.1:8081), iPhone HTTPS handshakes fail : 'Client TLS handshake failed... does not trust the proxy's certificate for clients4.google.com' Reason : mitmproxy 11 auto-generated its OWN CA at startup in /etc/secubox/toolbox/ca-wg/mitmproxy-ca-cert.pem (subject CN=mitmproxy). This is DIFFERENT from the CA I pre-generated (subject CN=Gondwana ToolBoX WG CA). mitm uses its own keypair, not mine. iPhone has the R1/R2 CA installed but NOT this mitm-wg CA → all HTTPS through R3 tunnel breaks. ## Fix ### /wg/ca.pem (NEW) Serves /etc/secubox/toolbox/ca-wg/mitmproxy-ca-cert.pem as PEM with Content-Type: application/x-x509-ca-cert. Android/PC manual install. ### /wg/ca.mobileconfig (NEW) iOS profile that bundles the CA root for one-tap install in iPhone trust store. Same format as /ca/mobileconfig but with the mitm-wg CA inside. ### R3 install page update The warning block on /wg/r3-install now shows : ⚠ Important pour R3 : il faut installer le certificat R3 spécifique (différent du certificat R1/R2). [📥 Installer CA R3 iPhone (.mobileconfig)] [🤖 Télécharger CA R3 (.pem Android/PC)] User flow now : 1. Install WG profile (/wg/profile/new or /wg/qr.png scan) 2. Install R3 CA (/wg/ca.mobileconfig) 3. Toggle WG tunnel ON 4. Surf HTTPS → mitm decrypts → addons fire → banner inject ## R3 banner injection verified live mitm-wg.service running on 10.99.1.1:8081 with 7 addons. Logs show : 10.99.1.6:54868: GET http://82.67.100.75/manifest.json << 200 OK 438b Client TLS handshake failed... (iPhone CA missing — fixed by this commit) iPhone WG tunnel transferring 7.58 MiB to client + 906.85 KiB from client. |
|||
| 7d2bfca881 |
feat(secubox-toolbox): /admin/ webUI with rich client table + level switch modal + R3 mitm-wg.service started + DNAT re-enabled (ref #496)
## R3 banner injection enabled live - systemctl enable + start secubox-toolbox-mitm-wg.service - Now LISTENING on 10.99.1.1:8081 (PID 216413) - nft DNAT prerouting re-enabled : tcp 443/80 → 10.99.1.1:8081 - iPhone WG packets : tunnel decap → DNAT → mitm transparent → addons fire - banner inject_banner.py active on text/html responses - 7 addons running (local_store + inject_banner + dpi + cookies + avatar + ja4 + soc_relay) - CA = /etc/secubox/toolbox/ca-wg (separate from R1/R2) ## /admin/ webUI (NEW) GET /admin/ + /admin returns a self-contained HTML page with : - 6 KPI cards (Total / Actifs / R1 / R2 / R3 / Risque LOW) - Client table : Status emoji · Device · Hash · IP · Level chip · Risk · Last seen · Actions - 🔀 Override button per client → modal with 4 level buttons (R0/R1/R2/R3) - 📄 PDF download per client (admin endpoint) - Auto-refresh every 15s - JS POSTs to /admin/clients/{mh}/level for the level change CSS aligned with cabine charte (P31 phosphor + cosmos black + purple accents). Modal centered overlay with 4 colored level buttons. User direction : 'injection banner en r3... webui level r switch'. |
|||
| 829c87ce0d |
fix(wg-provision): allow UDP 51820 in inet filter input + forward chain + disable DNAT by default (ref #496)
## CRITICAL E2E bug discovered
User reported : 'vpn kbinn depuis iphone ok mais pas de surf'.
Diagnostic on gk2 :
- WG kernel module : 3 peers registered
- tcpdump on lan0 : iPhone UDP handshake packets ARRIVING (148 bytes)
- wg show wg-toolbox : NO peer with endpoint or handshake → never completes
- nft list chain inet filter input : policy DROP + NO udp dport 51820 accept rule
Root cause : the default inet filter input chain has policy=drop and
explicit accepts only for SSH, captive ports, conntrack, and LAN bridges
— NO accept for UDP 51820. WG handshake packets were silently dropped
BEFORE reaching the wireguard kernel module.
## Fix
### Add UDP 51820 to inet filter input
The provision script now adds :
nft add rule inet filter input udp dport ${WG_PORT} accept
Verified live : after adding the rule, peer P2e3acAl8Tf8uQtAdLsh/...=
showed :
endpoint: 192.168.1.254:55214 (iPhone via DMZ port-forward)
latest handshake: 8 seconds ago
transfer: 906.85 KiB received, 7.58 MiB sent
= iPhone NOW SURFS through gk2 over WireGuard tunnel ✓.
### Disable DNAT prerouting by default
The Phase 6.C DNAT rules (tcp 443/80 → 10.99.1.1:8081 for mitm
interception) are now COMMENTED OUT in the provision script. Reason :
when DNAT is active but mitm-wg systemd service isn't running on 8081,
ALL surf breaks (packets get DNAT'd to nowhere).
To re-enable mitm interception, operator must :
1. Ensure secubox-toolbox-mitm-wg.service is running (mitm on 8081)
2. Uncomment + apply the DNAT rules in the script
3. Test surf still works (with mitm = banner/analysis active)
Until then : R3 = pure WG VPN tunnel through gk2 (analysis OFF).
### Add inet wg-toolbox forward chain
Explicit accept rules for wg-toolbox <-> lan0 forwarding. Belt-and-braces
in case any global firewall ever changes default policy.
## E2E status (gk2 2026-06-05)
iPhone WG handshake : ✓ completed
Tunnel up : ✓ MB/s transferring
Internet egress : ✓ via lan0 masquerade
mitm interception : DISABLED (Phase 6.D follow-up)
## Phase 6.D plan
- Start secubox-toolbox-mitm-wg.service permanently
- Re-enable DNAT prerouting
- iPhone Safari → mitm decrypt → banner inject + addon flows
- Same multi-peer model (wg-quick) + per-flow mitm (transparent)
- QUIC interception via mitmproxy 11 native HTTP/3 support
|
|||
| d5ab8c65c4 |
feat(secubox-toolbox): MAJOR RELEASE polish — cumulative anonymous stats + public landing page + HAProxy vhost + admin clients rich + admin level override (ref #496 #497)
## Major release polish package
User direction : 'travaillons bien stp', 'major release', and stacked
requests for landing page + visual graphs + cumulative stats + admin
richesse + admin override.
### secubox_toolbox/cumulative.py (NEW)
Anonymous cumulative stats aggregator. Recomputes every 60s with
external cache file at /var/lib/secubox/toolbox/cumulative-cache.json.
Exposes :
sessions : last_24h / 7d / 30d / all_time (distinct mac_hash counts)
events : per-source counts (dpi, cookies, ja4, soc) over 7d
top_hosts_7d: 15 most-seen hosts (anonymized — no mac_hash ties)
risk_distribution_7d : {low, medium, high} counts
level_distribution_7d: {r0, r1, r2, r3} counts
ALL hashes + IPs already anonymized — these stats add another layer of
aggregation on top.
### Endpoints :
GET /cumulative-stats.json : public JSON (no auth, no per-user data)
GET /landing : public HTML landing page
GET /cabine : alias for /landing
### Public landing page kbin.gk2.secubox.in
templates/landing.html.j2 — full marketing-grade page with :
- Hero with project name + tagline + 2 CTAs
- 4 KPI cards (sessions + connexions + hôtes + cookies, live from cumulative)
- Pitch paragraph (CyberMind / CSPN ANSSI / LCEN)
- 4 levels visual grid (R0 grey / R1 green RECOMMANDÉ / R2 amber / R3 purple NOUVEAU)
- SVG inline charts :
* risk distribution stacked bar (low/medium/high %)
* level distribution horizontal bars per opt-in choice
- Architecture ASCII diagram (3 LXC mitm disjoints)
- Demo R3 install (5-step ordered list)
- Open Source CMSD-1.0 + GitHub link
- Contact + soutiens (Liberapay + déploiement collectivité)
- Footer CyberMind / Notre-Dame-du-Cruet
P31 phosphor theme aligned with cabine charte. Responsive grid.
manifest.json linked for PWA install.
### HAProxy vhost kbin.gk2.secubox.in
Added to /etc/haproxy/haproxy.cfg :
acl host_kbin_gk2_secubox_in hdr(host) -i kbin.gk2.secubox.in
use_backend toolbox_landing if host_kbin_gk2_secubox_in
backend toolbox_landing
mode http
server toolbox 10.99.0.1:8088 check
HAProxy reloaded successfully. *.gk2.secubox.in wildcard cert covers
HTTPS termination.
### GET /admin/clients/rich (NEW)
Enriched client list with pseudo icons + statuses for the admin webui :
- device_emoji (📱 placeholder)
- level_emoji (🌐 R0 / 🛡 R1 / 🔍 R2 / 🌐 R3)
- status_emoji (🟢 actif < 5min / 🟡 idle < 1h / ⚪ expiré / 🔴 quarantine)
- risk_emoji (🟢 / 🟡 / 🔴 by score)
- level chip + risk chip
### POST /admin/clients/{mac_hash}/level (NEW)
Admin override of a client's level (operator-only). Updates store.level
(read by inject_banner). Note : nft sets not auto-updated — operator
must trigger client's own /change-level OR manually edit nft.
### splash + dashboard wg_enabled flag
Both templates now receive wg_enabled = (server.pubkey exists) so the
R3 button + dashboard link render conditionally.
### dashboard cumulative pass-through
report-live.html.j2 receives cumulative dict for the upcoming SVG
charts inside the dashboard (will be wired in follow-up commit).
|
|||
| 7aa1c62345 |
feat(secubox-toolbox): Phase 6.C — wg-quick + transparent mitm DNAT architecture + secubox-toolbox-mitm-wg.service (ref #496)
## Architecture decision
mitmproxy 11 --mode wireguard is SINGLE-PEER only (one client at a time
per mitm instance). Not suitable for the cabine multi-tenant use case
where multiple R3 users connect simultaneously.
Better architecture (this commit) :
iPhone WG tunnel
│
│ UDP 51820 via DMZ → 82.67.100.75
▼
gk2 : wg-quick@wg-toolbox kernel iface (MULTI-PEER, native WG)
│ decapsulates WG → packets emerge on 10.99.1.0/24
▼
nft inet wg-toolbox prerouting :
iif wg-toolbox tcp dport 443 dnat to 10.99.1.1:8081
iif wg-toolbox tcp dport 80 dnat to 10.99.1.1:8081
▼
mitmdump 11.0.2 --mode transparent on 10.99.1.1:8081
│ + 7 addons (local_store/inject_banner/dpi/cookies/avatar/ja4/soc_relay)
│ + CA /etc/secubox/toolbox/ca-wg/ (SEPARATE from R1/R2 transparent CA)
▼
nft postrouting masquerade lan0 → internet
## What this commit adds
### sbin/secubox-toolbox-wg-provision updated
Adds the DNAT chain + rules to the inet wg-toolbox nft table :
nft add chain inet wg-toolbox prerouting { type nat hook prerouting priority dstnat; }
nft add rule inet wg-toolbox prerouting iif wg-toolbox tcp dport 443 dnat to 10.99.1.1:8081
nft add rule inet wg-toolbox prerouting iif wg-toolbox tcp dport 80 dnat to 10.99.1.1:8081
Plus all the existing postrouting masquerade rules.
### debian/secubox-toolbox-mitm-wg.service (NEW)
Systemd unit for the R3 mitm process :
- Type=simple, User=secubox-toolbox, ambient caps NET_ADMIN + NET_RAW
- After/Requires wg-quick@wg-toolbox.service
- mitmdump 11 from /opt/mitmproxy-toolbox/bin (pip venv, mitmproxy>=11)
- --mode transparent --listen-host 10.99.1.1 --listen-port 8081
- --set confdir=/etc/secubox/toolbox/ca-wg (dedicated CA, disjoint from R2 CA)
- 7 addons inherited from host bind-mounts
- ProtectSystem=strict + ReadWritePaths whitelist
- MemoryMax 512M (R3 = heavier than R2 since QUIC + TCP both pass through)
To be enabled by postinst when LXC isn't available (host-native R3 fallback).
## E2E verified gk2 2026-06-05
wg-quick@wg-toolbox : active
UDP 51820 listening (multi-peer)
nft inet wg-toolbox postrouting masquerade lan0
nft inet wg-toolbox prerouting DNAT 443/80 → 10.99.1.1:8081
mitmdump --mode transparent on 10.99.1.1:8081 : listening + addons loaded
Architecture LIVE. iPhone WG client can :
1. Scan /wg/qr.png
2. WG app imports profile (privkey + Address 10.99.1.X + endpoint kbin.gk2.secubox.in:51820)
3. Toggle WG ON → handshake via UDP 51820
4. All TCP 80/443 packets get DNAT'd to mitm → addons fire → banner inject
## Out of scope (Phase 6.D follow-up)
- Make the WG mitm an LXC container (currently host-native for simplicity)
- UDP/QUIC interception (mitm transparent on 8081 only catches TCP)
- Per-R3 client level switching (R3 = always 'mitm active'; no R3a/R3b yet)
- kill switch on disconnect (currently iPhone disconnect = nft state stays)
|
|||
| 68cd8c7036 |
fix(wg-provision): pure nft masquerade + skip iptables (gk2 native nft) + perms 0750 root:secubox-toolbox (ref #496)
## Bug discovered E2E
User opened ISP firewall via DMZ → UDP 51820 reachable. Provisioning ran
on the board, BUT wg-quick failed :
iptables: Extension MASQUERADE revision 0 not supported, missing kernel module
iptables-nft RULE_APPEND failed
wg-quick exit 4/NOPERMISSION
Root cause : gk2 uses pure nft (iptables-nft compat layer unreliable on
this kernel). The PostUp iptables MASQUERADE rule never installed.
Plus : /etc/secubox/toolbox/{wg,ca-wg} were 0700 root:root → secubox-toolbox
user couldn't read server.pubkey + ca-wg/ca.pem from /wg/profile/new
endpoint.
## Fix
### wg-toolbox.conf : strip iptables PostUp
The conf only declares [Interface] (PrivateKey + Address + ListenPort).
NAT is set up natively via nft :
nft add table inet wg-toolbox
nft add chain inet wg-toolbox postrouting { type nat hook postrouting priority srcnat; }
nft add rule inet wg-toolbox postrouting ip saddr 10.99.1.0/24 oif lan0 masquerade
### sysctl persistence
Already done — /etc/sysctl.d/99-secubox-wg.conf sets net.ipv4.ip_forward=1.
### Perms 0750 root:secubox-toolbox
install -d -m 0750 -o root -g secubox-toolbox $WG_DIR $CA_WG_DIR
Aligns with how /etc/secubox/secrets/toolbox-mac-salt is owned (Phase 3
salt access fix).
## E2E live verified gk2 2026-06-05
systemctl is-active wg-quick@wg-toolbox → active
wg show wg-toolbox →
interface: wg-toolbox
public key: 6JmT185TPc4aHjwDjC8ugKBmehsrGSMyw1V+SOG40XI=
listening port: 51820
peer: JoxT1YcwW9X5xTL9xjkOq6NcFjgQGO4jP1lxJ5iUBHI=
allowed ips: 10.99.1.3/32
curl http://10.99.0.1:8088/wg/profile/new → HTTP 200
.conf served with Endpoint = kbin.gk2.secubox.in:51820
Client IP = 10.99.1.3/32
Bundled CA root pem (separate from transparent mitm CA)
## Limitation noted (Phase 6.C)
The wg-toolbox interface decrypts WG tunnel + masquerades to lan0 →
internet DIRECTLY. mitm has NOT been wired yet :
- mitm container 'toolbox-mitm-wg' @ 10.100.0.62 needs to be provisioned
- mitmdump --mode wireguard:server.conf reads /etc/wireguard/wg-toolbox.conf
directly INSTEAD of wg-quick
- Conflict resolution : either we run wg-quick (current, no mitm) OR
mitmdump WG mode (planned, with mitm) — not both.
To wire mitm interception, Phase 6.C will :
1. Stop wg-quick@wg-toolbox
2. Start toolbox-mitm-wg LXC + mitmdump --mode wireguard inside
3. mitmdump becomes the WG endpoint; clients connect to same kbin.gk2.secubox.in:51820
User can already test the WG tunnel as-is (R3 = tunnel via cabine, NO
analysis yet). Phase 6.C will enable the analysis flow.
|
|||
| 455def53cc |
feat(secubox-toolbox): WG endpoint = kbin.gk2.secubox.in (Gandi-managed) + DNS record provisioned via API (ref #496)
## DNS provisioning via Gandi LiveDNS API User direction : 'go with dns api'. Found gk2.net is on OVH (no credentials available), but secubox.in is on Gandi LiveDNS with API token in /var/lib/secubox/dns-provider/ providers.json (secubox-dns-provider service). Created DNS record via Gandi LiveDNS API : POST https://api.gandi.net/v5/livedns/domains/secubox.in/records Header: Authorization: Bearer <token> Body: {rrset_name: 'kbin.gk2', rrset_type: 'A', rrset_ttl: 300, rrset_values: ['82.67.100.75']} Response: 'DNS Record Created' Verified : dig kbin.gk2.secubox.in @ns1.gandi.net → 82.67.100.75 (gk2 pub IP) Pattern aligns with the rest of *.gk2.secubox.in (arm, armada, admin, analyse, etc. — all A → 82.67.100.75). ## Code updates - secubox_toolbox/wg.py : WG_ENDPOINT = 'kbin.gk2.secubox.in' (was 'kbin.gk2.net') - secubox_toolbox/api.py : /wg/status endpoint label same This is what gets baked into client .conf files served by /wg/profile/new and into the QR codes at /wg/qr.png. ## Reverse-DNS / TLS The wildcard *.gk2.secubox.in cert (Let's Encrypt) already covers kbin.gk2.secubox.in for any HTTPS service we'd later expose on that host. WG itself is UDP and doesn't need cert. ## Still pending (user) - ISP firewall : open UDP 51820 inbound to 82.67.100.75 (need port forward at home router OR ISP modem) - drop docs/assets/poster/village3b-A2.png (rendered poster image) ## Next code commit (Phase 6.C) - debian/postinst : call secubox-toolbox-wg-provision after lxc-provision - debian/control : Depends += wireguard-tools, wireguard - debian/install : ship sbin/secubox-toolbox-wg-provision - toolbox-up CLI : detect wg-toolbox running + render nft.mitm.target - E2E test on board |
|||
| f77bb4577f |
feat(secubox-toolbox): Phase 6.B — wg-provision script + R3 nft helpers + R3 in /accept + /change-level + dashboard R3 link (ref #496)
## Phase 6.B wiring
### sbin/secubox-toolbox-wg-provision (NEW, executable)
Idempotent provisioning script :
1. Server keypair @ /etc/secubox/toolbox/wg/{server.privkey,server.pubkey}
- umask 077 + chmod 0600 priv / 0644 pub
- keeps existing if already present (no rotation surprise)
2. wg-quick conf @ /etc/wireguard/wg-toolbox.conf :
- Address 10.99.1.1/24 + ListenPort 51820
- PostUp/PostDown iptables FORWARD + MASQUERADE to lan0
- Peers added dynamically by /wg/profile/new
3. CA WG dedicated root @ /etc/secubox/toolbox/ca-wg/ :
- SEPARATE from transparent mitm CA (/etc/secubox/toolbox/ca/)
- Maintains the disjoint-from-toolbox-mitm-tx invariant
- openssl req self-signed CN=Gondwana ToolBoX WG CA
4. systemctl enable wg-quick@wg-toolbox (start best-effort)
5. nft add set consented_r3_wg_macs (ether_addr, timeout 24h)
To be called by debian/postinst after Phase 5 LXC provision.
### nft.py helpers (R3 trio)
add_r3_wg(mac, ttl=24h) -> bool
del_r3_wg(mac) -> bool
is_r3_wg(mac) -> bool
Same pattern as add_r2_banner / del_r2_banner / is_r2_banner.
### nft-toolbox.nft.j2 : new set consented_r3_wg_macs
Membership = client installed WG profile + connected via wg-toolbox iface.
Captive DNAT does NOT apply (R3 traffic routes via 10.99.1.0/24 + WG
tunnel, not via the captive AP iface).
### /accept + /change-level : R3 support
Accept level=r3 in form data. R3 only allowed if /etc/secubox/toolbox/wg/
server.pubkey exists (gate by provisioning state).
Level transitions :
r0 -> r1/r2 : add to consented_r2_macs
r1/r2 -> r3 : remove from r2_banner_macs, add to consented_r3_wg_macs
r3 -> r0/r1/r2 : remove from consented_r3_wg_macs
Log line now includes wg=True/False alongside validated/consented/banner.
### Splash R3 link (existing button updated)
Splash already had R3 button from earlier commit (Phase 6.A) — it links
to /wg/r3-install which provides QR + .conf + instructions.
### Dashboard R3 link in level switcher
Below the R0/R1/R2 grid : a separate '🌐 installer le profil' link
(R3 doesn't fit the grid because it's a different installation flow,
not a one-tap nft membership change).
If current_level == 'r3' : confirmation message '✓ Tu es actuellement
en mode R3 — tunnel WG actif'.
## Remaining for Phase 6 PR readiness
- [ ] toolbox-up CLI : detect WG container running + set mitm.target accordingly
- [ ] debian/postinst : call wg-provision after lxc-provision
- [ ] debian/control : Depends += wireguard-tools, wireguard
- [ ] E2E test : provision wg-toolbox + /wg/profile/new + iPhone scan QR ->
connect -> packet flows through mitm -> banner injects
- [ ] kbin.gk2.net DNS A record (user task)
- [ ] UDP 51820 firewall open (user task)
|
|||
| 4e90f2970e |
fix(secubox-toolbox): Android cert PEM format + Android install help + PWA manifest + R3 install page (ref #496)
## User feedback
'android certificat error, pas de webapp fix it'
'ok r3'
'go'
'qr code du splash okay'
## Android cert fix (PEM instead of DER)
Was : /ca/android.crt returned DER binary blob -> Android 11+ Chrome had
intermittent install issues + 'file format unknown' errors.
Now :
/ca/android.crt -> PEM text (Android-friendly, base64 + headers)
/ca/android.der -> DER fallback for older Android / manual install
ca.py adds ca_pem() helper alongside ca_der().
MIME = application/x-x509-ca-cert (triggers Chrome install prompt).
## /ca/android-help (NEW)
Self-contained HTML page (no template dep) with step-by-step Android
install flow :
1. Download .crt button (calls /ca/android.crt)
2. Settings -> Security -> Encryption -> Install certificate -> CA
3. Verify SHA-1 fingerprint (fetched via JS from /ca/fingerprint)
4. Honest warning : Android 7+ requires apps to opt-in to user CAs
via network_security_config.xml — banks/gov/streaming usually
don't, so they'll fail in R1/R2. Use R0 or R3 instead.
## PWA manifest /manifest.json (Android webapp install)
Android Chrome reads /manifest.json + offers 'Add to Home Screen' = PWA.
Same UX as iOS WebClip but cross-platform.
name: 'ToolBoX Cabine VILLAGE3B'
short_name: 'ToolBoX'
start_url: '/report/me/html'
display: 'standalone' (no Chrome UI when launched from home)
theme_color: '#00dd44' (P31 phosphor green)
background_color: '#0a0a0f' (cosmos black)
icons: /qr/splash.png (232x232)
## R3 install page /wg/r3-install (NEW)
Phase 6 user-facing install page with 2 methods :
Method 1 (recommended) : Scan QR
- iPhone : WireGuard app -> Add tunnel -> Scan from QR
- QR contains the full .conf (server pubkey + endpoint + private key)
- Privacy warning : 'Le QR contient ta clé privée. Ne le partage pas.'
Method 2 : Download .conf
- iPhone : open file -> WireGuard app -> Import
- Android : WireGuard app -> Import from file
- Linux : wg-quick up village3b-toolbox.conf
Plus warning : install CA root first (mobileconfig iPhone OR .crt Android)
or HTTPS sites will fail once WG tunnel is up.
Plus after-connect explanation : tunnel active -> banner everywhere
(incl QUIC) -> report live -> iCloud/Push still work via routing.
## E2E verified gk2 2026-06-05
/ca/android.crt HTTP 200 (PEM, starts with -----BEGIN CERTIFICATE-----)
/ca/android.der HTTP 200 (DER binary)
/ca/android-help HTTP 200 (HTML)
/manifest.json HTTP 200 (JSON)
/wg/r3-install HTTP 200 (HTML)
QR splash already validated by user 2026-06-05 ('qr code du splash okay').
Same QR endpoints used by R3 install page.
|
|||
| 173b4262df |
feat(secubox-toolbox): Phase 6 foundation — WireGuard R3 mode (LXC + service + wg.py + endpoints + splash button) (ref #496)
## Architecture target Two distinct LXC containers for ToolBox MITM : - toolbox-mitm (R1/R2 transparent) : 10.100.0.61 (Phase 5 #495) - toolbox-mitm-wg (R3 WireGuard) : 10.100.0.62 (this commit) Plus the existing WAF mitm : 10.100.0.60 (#495 untouched isolation). User direction : 'il doit y avoir 2 mitm lxc separes... distantes et disjointes' + 'go genial' for WireGuard mode. ## What this commit adds ### lxc/toolbox-mitm-wg.conf.template LXC config for the WG container. Distinct hwaddr 00:16:3e:00:00:62 + IP 10.100.0.62 + start order 120 (after WAF=100, tx=110). Memory bounds heavier (600M/768M) since WG mode decapsulates QUIC in user-space. Tun device + WG kernel module access. Bind mounts use a SEPARATE CA dir (/etc/secubox/toolbox/ca-wg) to maintain the disjoint-from-transparent-mitm invariant. ### lxc/toolbox-mitm-wg.service.template systemd unit inside container : mitmdump --mode wireguard:/etc/secubox/toolbox/wg/server.conf --listen-host 0.0.0.0 --listen-port 51820 + 7 addons (same as transparent mode for unified analysis) Requires=wg-quick@wg-toolbox.service so the WG kernel interface is up before mitm tries to use it. ### secubox_toolbox/wg.py WG profile generation library : generate_client_profile(label) -> {client_privkey, client_pubkey, client_ip (10.99.1.X), server_pubkey, endpoint (kbin.gk2.net:51820), dns (10.99.0.1), conf_text (wg-quick format), ca_pem (bundled)} revoke_client(pubkey) -> removes peer from wg + DB Persisted state in /var/lib/secubox/toolbox/wg-peers.json : {next_ip_octet, peers : {pubkey : {ip, label, created_at}}} ### FastAPI endpoints GET /wg/profile/new -> .conf download (attachment) GET /wg/qr.png -> QR encoding the .conf (iOS WG app scans it) GET /wg/status -> peer count + handshake info All ImportError-soft : if wg module missing (Phase 6 not provisioned on this board), endpoints return 503 gracefully. ### Splash R3 button New R3 level button below R2 (only rendered if wg_enabled config flag). Purple style, label 'WireGuard portable (NOUVEAU)' + subtitle : 'VPN tunnel mitm. Bandeau sur TOUT (HTTPS + QUIC). Marche hors VILLAGE3B (4G/5G, autre WiFi). Profile install 1 tap.' ## Next steps (separate commits in this PR) - [ ] sbin/secubox-toolbox-wg-provision : generate server keypair + wg-quick config + register systemd wg-quick@wg-toolbox unit - [ ] /wg/r3-install : HTML page that triggers profile download + shows QR code + install instructions for iOS WG app - [ ] /accept handler : add level=r3 support (adds MAC to a new nft set consented_r3_wg_macs, persists in store) - [ ] /change-level : R3 switch from dashboard - [ ] toolbox-up CLI : detect WG container + populate config - [ ] Endpoint DNS : kbin.gk2.net needs A record to gk2 public IP - [ ] UDP 51820 firewall : open at ISP / gateway - [ ] mobileconfig wrapper for .conf (iOS one-tap install instead of manual import in WG app) - [ ] E2E test : iPhone scans QR -> WG connects -> mitm decrypts -> banner ## Out of scope (this commit) Existing R0/R1/R2 levels untouched. WAF mitm container untouched. Phase 5 (#495) LXC infrastructure stays as foundation. |
|||
| 3d2ecd574f |
feat(secubox-toolbox): 3 QR code endpoints + splash QR display + 9th dashboard widget (device fingerprint) + README poster integration (ref #495 #497)
## QR endpoints (python-qrcode)
GET /qr/splash.png → QR encodes the splash URL (Cabine join)
GET /qr/cert.png → QR encodes /ca/mobileconfig (iOS profile)
GET /qr/webclip.png → QR encodes /ca/webclip-cabine.mobileconfig
GET /qr/{target}.png → generic : maps splash/cert/webclip/report/
fingerprint to fixed URLs, or encodes literal
PNG output, Cache-Control: public, max-age=3600.
Verified : 232x232 1-bit grayscale, 384-553 bytes each.
debian/control adds python3-qrcode dependency.
## Splash QR card
The cert install card on the splash now shows 3 QR codes (① SPLASH,
② CERT iPHONE, ③ WEBCLIP HOME) on a white background grid below the
test button. Scannable from another device for cross-installation
(useful at the cabine : user shows QR to others without typing URLs).
## Dashboard hero : 9th widget (device fingerprint)
Mirroring the poster layout, added a full-width 9th widget below the
2 rows of 4 KPI widgets :
┌─────────────────────────────────────────────────────────┐
│ 📱 📱 Empreinte device │
│ iPhone iOS 26.3 · 🟢 Chrome 148 │
│ 21 UAs distincts observés │
└─────────────────────────────────────────────────────────┘
Now matches the 9-widget metrics layout from the public poster :
🌐 connexions · 📡 hôtes · ✅ OK 2xx · 🔒 cert-pin
📺 apps · 🍪 trackers · 🌍 pays · 🎯 score
📱 device (full-width)
## README + assets/poster integration (#497)
- README.md : new section at top with poster image embed + 3 doc links
- docs/assets/poster/README.md : print specs + variants list + license
- Placeholder docs/assets/poster/village3b-A2.png expected (user provides)
Wiki integration pending : will be added via gh wiki API once issue #497
phase 2 launches.
## Rebase
Branch was rebased onto master (which now has all PR #493 Phase 3
commits). control merged : Phase 3 fonts + Phase 5 lxc deps + new
python3-qrcode in one cohesive Depends block.
|
|||
| 448aee77e9 |
feat(secubox-toolbox): Phase 5 wiring — postinst + control + install + nft DNAT migration (ref #495)
On configure :
- if lxc-create + provision script available -> run secubox-toolbox-lxc-provision
- on success : disable + stop host-native secubox-toolbox-mitm.service
(LXC container takes over)
- on failure : keep host-native enabled (graceful fallback)
- if LXC entirely absent (dev env, container-less host) : log + keep
host-native (legacy mode)
This means upgrade path is :
Phase 3 board (host-native) -> apt upgrade -> Phase 5 board
-> postinst detects lxc -> provisions container -> stops host-native
-> container takes over with ZERO user intervention.
Depends += lxc, debian-archive-keyring
Recommends: bridge-utils
debian-archive-keyring needed for lxc-create --release=bookworm signing.
lxc/toolbox-mitm.conf.template -> /usr/share/secubox/toolbox/lxc/
lxc/toolbox-mitm.service.template -> /usr/share/secubox/toolbox/lxc/
sbin/secubox-toolbox-lxc-provision -> /usr/sbin/
prerouting_nat rule 2 (R2 MITM redirect) now uses a templated target :
dnat ip to {{ mitm.target | default(dhcp.gateway) }}:8080
- Default value = gateway (host-native legacy mode)
- LXC mode = '10.100.0.61' (container)
- Phase 6 WireGuard = '10.100.0.62' future
toolbox-up CLI will need to populate mitm.target based on whether the
LXC container is running.
Phase 5 foundation + wiring done. Remaining for E2E :
- FastAPI _pull_mitm_module_events socket paths : currently host
sockets at /run/secubox/{module}.sock — these stay on host, the
container only does mitm interception. No change needed there.
- toolbox-up CLI : detect LXC + set mitm.target accordingly
- E2E test on board : provision + restart + R2 surf + banner inject
To deploy on gk2 :
apt update && apt install ./secubox-toolbox_X.X.X_all.deb
systemctl restart secubox-toolbox
Verify :
lxc-info -n toolbox-mitm | grep RUNNING
lxc-attach -n toolbox-mitm -- systemctl status secubox-toolbox-mitm
|
|||
| 57cf73d117 |
feat(secubox-toolbox): Phase 5 foundation — LXC container template + provisioning script (ref #495)
## Goal Move the ToolBox transparent mitm from host-native (current Phase 3) to a dedicated LXC container 'toolbox-mitm' at 10.100.0.61. Foundation for Phase 6 (#496 WireGuard mode) which will live in a SECOND container 'toolbox-mitm-wg' at 10.100.0.62. User direction : 'il doit y avoir 2 mitm lxc separes... distantes et disjointes' + 'go genial'. ## What this commit adds ### lxc/toolbox-mitm.conf.template LXC container config for the transparent mitm. Same envelope as the existing WAF mitm container (memory.high 400M, memory.max 512M, autostart, br-lxc bridge) but : - distinct hwaddr 00:16:3e:00:00:61 - distinct IP 10.100.0.61 - distinct start order (110 = after WAF mitm 100) Bind mounts from host : /etc/secubox/toolbox/ca RO (CA persists on host) /var/lib/secubox/toolbox RW (SQLite events shared) /var/log/secubox/toolbox-mitm RW (logs) /usr/lib/secubox/toolbox/mitmproxy_addons RO (addons live on host) /etc/secubox/secrets/toolbox-mac-salt RO (MAC hashing salt) Capabilities dropped to minimum + tun device access for transparent / future wireguard mode. ### lxc/toolbox-mitm.service.template Systemd service that runs INSIDE the container. Type=simple, runs mitmdump --mode transparent with all 7 addons. Ambient caps NET_ADMIN + NET_RAW + NET_BIND_SERVICE. Sandbox via ProtectSystem=strict + ReadWritePaths whitelist. MemoryMax 480M inside container. ### sbin/secubox-toolbox-lxc-provision Idempotent provisioning bash script : 1. Prereq check (lxc-create + template exists) 2. Build rootfs if absent (Debian 12 bookworm arm64, ~80 MB) 3. Install/refresh /etc/lxc/toolbox-mitm.conf 4. Ensure bind-mount source paths exist on host 5. Deploy mitm service inside container 6. Restart container if running, start if stopped 7. Wait for network up To be called by debian/postinst of secubox-toolbox. ## Next steps (separate commits) - [ ] Update debian/postinst to call secubox-toolbox-lxc-provision - [ ] Update debian/control : Depends += lxc, debian-archive-keyring - [ ] Update FastAPI _pull_mitm_module_events socket paths or HTTP-bridge - [ ] Update nft DNAT prerouting rules : DNAT to 10.100.0.61:8080 instead of host 10.99.0.1:8080 - [ ] Stop + disable secubox-toolbox-mitm.service (host-native) at upgrade - [ ] Migration test : preserve user's installed CA across upgrade - [ ] E2E test : R1/R2 surf + banner injection still fires from container - [ ] Phase 6 prereq : second container 'toolbox-mitm-wg' for WireGuard mode ## Architecture note Per #495 issue : the two mitm instances are FULLY DISJOINTES. - WAF mitm : LXC 'mitmproxy' @ 10.100.0.60 (existing, untouched) - ToolBox tx : LXC 'toolbox-mitm' @ 10.100.0.61 (this commit) - ToolBox wg : LXC 'toolbox-mitm-wg' @ 10.100.0.62 (#496 future) |
|||
|
|
2558e28f17
|
Merge pull request #493 from CyberMind-FR/feature/492-phase-3-toolbox-transparency-layer-white
feat(phase-3): transparency layer (whitelist + analysis-status + security quality scoring) |
||
| 2efdd82bbf |
docs(marketing): poster grand public + dossier presse update Phase 3+5+6 (ref #492 #480)
## 2 docs marketing ### POSTER-grand-public-village3b.md (NEW) Brief de poster A2 portrait pour affichage public (médiathèque, école, mairie, EHPAD). 6 zones : 1. Titre 📡 VILLAGE3B + USP (anonyme + gratuit + open source) 2. 3 niveaux d'opt-in R0/R1/R2 visualisés 3. Ce que contient le rapport (KPI + categories + transparency) 4. Conformité CSPN/LCEN + 5 garanties 5. 3 QR codes (splash, cert, webclip) 6. Footer CyberMind + contact + soutiens Variantes A4/A5/dépliant 3-volets documentées. Charte couleurs P31 phosphor green + Cosmos Black + Gold Hermétique alignée avec DESIGN-CHARTER.md. Output formats à générer : PDF print 300dpi, SVG Inkscape source, PNG 1920x1080 social, Story IG 1080x1920, LinkedIn banner. ### PROMPT-claude-presse-gouv.md (UPDATED) Ajout d'une ANNEXE 'Mise à jour 2026-06-05' qui synchronise le dossier presse avec l'évolution récente : - Modèle d'opt-in à 3 niveaux + R3 WireGuard (Phase 6) - Transparence radicale : 'La cabine SAIT se taire, et DIT QUAND elle se tait' — différenciateur fondamental - Architecture mitm disjoints (Phase 5 #495 séparation host/LXC) - Mode WireGuard portable kbin.gk2.net:51820 (Phase 6 #496) pour usage HORS cabine physique = inclusion territoriale + EHPAD/domicile - Engine sensibilité réglable (low/medium/high/paranoid) : opérateur collectivité adapte sans toucher code - Empreinte hardware MochaBin + Debian + coût ~250€ HT - AI-HANDOVER brief = continuité du commun numérique au-delà d'un auteur - 5 composants installables iPhone (cert + webclip + WG + guide) - Engagement personnel : 'pas de boîte noire, commun numérique' Tous ces éléments à intégrer par le rédacteur LLM dans la tribune, le CP, les candidatures France.gouv (ANCT/France-Services/Beta.gouv/ ANSSI/France 2030/CNIL). |
|||
| 17ff8c5cfb |
docs: AI-HANDOVER tech brief for external LLM (GPT-5/Gemini/Claude) (ref #492)
13-section self-contained brief : 1. Mission produit + licence CMSD strict 2. Stack technique exhaustif 3. 2 mitm DISJOINTES (ToolBox host vs WAF LXC) — règle d'or 4. Niveaux R0/R1/R2 + R3 roadmap 5. Phases 1->8 status 6. Endpoints API actuels + Phase 6 ajouts 7. Rule engine sensitivity profiles 8. Drapeaux + emojis (5 fonts embedded PDF) 9. Quick-up composants splash 10. Dette technique connue (#494, WAF, certs, perms) 11. Contributing rules (worktree + licence header + pas Claude/Anthropic ref + tests E2E + CSPN) 12. Status 2026-06-05 + roadmap actif 13. Contact Destiné à brief un autre assistant LLM sans contexte préalable pour contribuer en cohérence avec l'écosystème cabine VILLAGE3B. |
|||
| a9db31442e |
fix(secubox-toolbox): remove QUIC drop rule from nft template — was breaking R2 surf entirely (ref #492)
## Bug
The QUIC reject rule for @r2_banner_macs (UDP 443 reject port-unreachable)
was too aggressive. iPhone Safari + Apple/Google/Cloudflare/Fastly heavily
depend on QUIC for almost all HTTPS traffic. Forcing fallback to TCP
HTTPS :
- works for some sites (banner injection visible there)
- breaks for cert-pinned services (Apple Push, iCloud, mask.icloud.com)
- exhausts iPhone retry budget -> Safari shows 'Cannot connect' on
sites that QUIC was working for
User feedback : 'toujours pas de surf en R2'.
## Fix
Remove the nft QUIC reject rule entirely. The r2_banner_macs nft set
remains (used by inject_banner.py to gate banner injection on text/html
responses). The banner appears only on TCP HTTPS HTML responses (rare
but happen) — that's the honest cost of not breaking the iPhone.
## Splash R2 description updated
'Déchiffrement TLS + bandeau visible sur les pages Safari HTML.'
+ warning : 'Le bandeau n'apparaît que sur les pages HTML servies
en TCP (la plupart des apps iPhone utilisent QUIC et ne montreront
pas le bandeau).'
Honest UX : user knows R2 means partial banner visibility, not 'banner
everywhere' which the QUIC drop tried to force.
## Phase 5 (#495) opens
Issue opened : ToolBoX mitm in dedicated LXC container, separate from
the WAF mitm + host-native. Architectural follow-up.
|
|||
| 43f79ea9e0 |
fix(secubox-toolbox): honest cert verification — probe is heuristic + manual iOS steps + prominent SHA-1 fingerprint (ref #492)
## User feedback 'le certificat est installe en mode root active mais le dashboard splash di ko encore...' ## Root cause The auto-probe was hitting captive.apple.com which is cert-pinned by iOS internal code — even when our CA is installed in trust store, iOS treats this URL specially and the probe gave a misleading 🔴 result. Plus the probe was over-claiming : a 🟢 result actually meant 'HTTPS worked for this specific URL', NOT 'your CA is installed'. Pre-acceptance (R0 default), HTTPS doesn't go through our mitm, so the probe always passes regardless of CA install state — false confidence. ## Fix ### 1. Probe target : example.com favicon - Not cert-pinned by iOS - Simple, low-latency - Reliable for the heuristic check (post-R1/R2) ### 2. Honest text labels - 🟢 -> 'HTTPS OK — si tu es déjà en R1/R2, ton CA est installé. Vérifie quand même les étapes manuelles ci-dessous.' - 🔴 -> 'HTTPS échoue — soit pas de réseau, soit en R1/R2 SANS le CA installé. Suis les étapes manuelles ci-dessous.' - ❔ -> 'Timeout — relance le test ou vérifie manuellement' ### 3. Manual verification steps (4-line ordered list) The certain way to verify CA install on iPhone, prominent below the auto-test : 1. Réglages → Général → VPN et gestion d'appareils 2. Profil Gondwana ToolBoX CA doit apparaître → installer 3. Réglages → Général → Information → Réglages de confiance des certificats 4. Activer le toggle pour Gondwana ToolBoX CA ### 4. SHA-1 fingerprint big + visible The CA SHA-1 fingerprint is now displayed prominently in a styled box with a key emoji, so the user can visually compare to what iOS Settings shows when they tap the profile. ## Architecture note Proper verification would require a CA-signed HTTPS leaf cert served by the toolbox on 10.99.0.1:8443 — JS probes that, success = CA validated by browser. Would need nginx HTTPS + leaf cert generation. Not in scope of this commit but a future follow-up to make auto-test reliable pre-acceptance. |
|||
| c5a2d20575 |
fix(secubox-toolbox): splash always rendered (no auto-redirect) — user can re-install cert anytime (ref #492)
## Bug
Commit
|
|||
| 4a3c7c63d4 |
feat(secubox-toolbox): splash quick-up menu — 4 install buttons emoji (cert iPhone + Android + webclip + guide) (ref #492)
User feedback : 'faire un systeme de menu dans le gondwana toolbox splash pour installer les composant, je n'ai plus aces aux urls de l.icon et du certificat' 'des quick up buttons emojiz' The cert install buttons were inside the cert auto-check card and visually competed with the test button. Now a dedicated '📦 Quick install — composants ToolBoX' section above the level choice : ┌──────────────────┬──────────────────┐ │ 🔐 Certificat │ 🤖 Certificat │ │ iPhone │ Android/PC │ │ .mobileconfig │ .crt manuel │ ├──────────────────┼──────────────────┤ │ 📱 Icône Home │ 📜 Guide pas-à- │ │ iPhone │ pas │ │ 1-tap accès │ install help │ └──────────────────┴──────────────────┘ [🧪 Tester le certificat maintenant] [⏳ Auto-test en cours...] Empreinte CA SHA-1 : XX:XX:XX:... Buttons styled : primary : purple solid (#9e76ff) primary-alt : darker purple (webclip) outline : transparent w/ purple border (Android + guide) test : green outline (cert test) All have :hover lift effect (translateY -1px + box-shadow). Subtitle in each button (.qbtn-sub) explains what the link delivers. ## Out of scope today (user reported) - chess.maegia.tv and other WAF vhosts intermittent : LXC mitm container restarted ; works for 30s then hangs again. Needs deeper container/network debug separate from ToolBoX. |
|||
| a6754d7bcd | fix(secubox-toolbox): another jinja slicing-filter-result bug at line 124 (ref #492) | |||
| bdac481541 |
fix(secubox-toolbox): jinja syntax error — slicing filter result requires intermediate var (ref #492)
Template rendering failed with 'expected token end of print statement, got [['
on line 110 of report-live.html.j2 :
Top ASN : {{ top_geo.flag|default('') }} {{ top_geo.asn_org|default('?')[:30] }}
Jinja doesn't allow slicing the result of a filter directly. Fixed by
binding to an intermediate var :
{% set top_asn_raw = top_geo.asn_org if top_geo.asn_org else '?' %}
Top ASN : {{ top_geo.flag|default('') }} {{ top_asn_raw[:30] }}
|
|||
| cb236abb67 |
fix(secubox-toolbox): splash bug — already-validated users were stuck on stale success page (no cert card) + add hero widgets to HTML dashboard + filtering compromissions card + Cache-Control no-store (ref #492)
## Critical bug fix GET / handler routed ALREADY-VALIDATED users to success.html.j2 — the OLD success page WITHOUT the cert card auto-check. So returning users couldn't see the new install/test cert UI. They saw 'success.html.j2' with just the 4 action buttons + thought R0 was broken because no cert was installed. Now : validated users get a 303 redirect to /report/me/html (the dashboard) which has level switcher + transparency metrics + the new hero widgets. ## Cache-Control no-store iPhone Safari was caching the splash + dashboard aggressively. User saw stale 'no cert card / no metrics' versions even after deploys. Now : Cache-Control: no-store, no-cache, must-revalidate, max-age=0 Pragma: no-cache on splash + dashboard responses. Forces Safari to refetch every visit. ## Hero widgets in HTML dashboard Mirrors the PDF dashboard hero exactly : Row 1 widgets : 🌐 connexions / 📡 hôtes / ✅ OK / 🔒 cert-pinning Row 2 widgets : 📺 apps / 🍪 trackers / 🌍 pays / 🟢/🟡/🔴 score Bottom line : Top device · Top app · Top ASN with flag Plus risk badge color-coded in the title bar (green LOW / amber MEDIUM / red HIGH). ## Filtering compromissions card User asked 'ou sont mes filterings compromissions dans la webui ?'. Added a dedicated 🚨 card under hero showing : - Sensibilité actuelle (label + description from rule_engine) - Threat-intel feed matches count - DGA candidates count - Beaconing detected count - Connexions bloquées count (Phase 4 placeholder — 0 for now) Explanation note : 'Le moteur détecte mais ne bloque pas encore — passive transparency. R3 / Phase 4 wired plus tard.' ## Dashboard level switcher : active level highlighted Previously the R1 button was always styled 'active' (bold green) regardless of current_level. Now uses server-side current_level to pick which button gets the ✓ check + bold border + brighter background. R0/R1/R2 button styles dynamically reflect the actual state. ## Welcome / switched banner uses current_level Was using request_args.level which is stale after refresh. Now uses server-side current_level so the banner always reflects the truth. |
|||
| 751e0a3036 |
feat(secubox-toolbox): splash cert card — explicit install button (iPhone + Android) + manual test button + fingerprint display (ref #492)
## User feedback
'acces au certificat et test certificat ???'
'switch r0 et plus de surf ... probleme de switch... test cert okay ou pas ???'
The previous auto-check card was passive : user couldn't see how to
install the cert or re-test on demand. R0 was working (verified via
tcpdump : iPhone packets flowing through forward chain successfully).
The 'plus de surf' impression was probably the result of:
- Cached Safari content from a broken-cert R1/R2 attempt
- No explicit feedback on cert status
## Splash redesign
The cert card now has 4 distinct sections :
1. Explanation : 'Sans cert -> R1/R2 cassent HTTPS. R0 OK sans cert.'
2. Two big install buttons (grid 1fr 1fr) :
[📥 Installer (iPhone)] -> /ca/mobileconfig (purple bg, prominent)
[🤖 Télécharger CRT] -> /ca/android.crt (outlined)
3. Manual test button :
[🧪 Tester le certificat maintenant] -> re-runs JS probe
4. Live status display :
⏳ Auto-test en cours -> 🟢 OK / 🔴 NON installé / ❔ Timeout
5. CA SHA-1 fingerprint in monospace for manual comparison in
iOS Réglages > Général > VPN/Device Management.
The auto-probe runs 600ms after page load. Click 🧪 to re-run any time.
Probe target changed from www.google.com favicon to captive.apple.com
success.html — Apple's HTTPS captive probe is more reliable on iOS
(already on iOS trust path, low latency, no DNS hijack risk).
## Verified gk2
PDF 51326 -> not affected. Splash adds ~1.2kB of HTML+CSS+JS.
|
|||
| b0a7571b77 |
feat(secubox-toolbox): splash cert chain auto-check + WAF mitm restart fix doc (ref #492)
## User feedback 'je dois avoir acces au certificats et avoir un auto check sur le splash de la chaine cert' ## What changed splash.html.j2 gains a cert status card BEFORE the level buttons : ┌──────────────────────────────────────────────────────────────┐ │ ⏳ Vérification certificat racine ToolBoX... │ │ Empreinte attendue : 62:A1:E5:3B:1F:C2:B2:97:77:AE:BB:58…│ └──────────────────────────────────────────────────────────────┘ JS detects CA install status : - fetch /ca/fingerprint -> display SHA1 the user must verify in iOS Réglages > Général > VPN/Device Management - Image probe to https://www.google.com/favicon.ico : onload -> 🟢 'Certificat installé — R2 prêt' onerror -> 🔴 'Certificat NON installé — installer le profil' 3s timeout -> ❔ 'Statut indéterminé' - Badge color + background tint adjusted per state Helps user decide R2 vs R1 BEFORE clicking, preventing the failure mode 'picked R2, no cert, all TLS broken'. ## Out of scope (Phase 3 transparency) User also reported (#chess.maegia.tv 504) : - WAF mitmproxy LXC container stuck -> restarted, port 8080 back - secubox-streamlit-routes failing : 'tee /srv/mitmproxy/haproxy-routes.json: Permission denied' -> infra drift (related to #494 / secubox-mesh perms) - chess.maegia.tv backend on gk2:8900 not listening -> Streamlit app stopped, separate concern These are WAF/Streamlit infra concerns separate from ToolBoX Phase 3. Will need their own issue/PR. |
|||
| 26b77e10cb |
fix(secubox-toolbox): splash redesign — recommandé badge + R2 warning out of form + cleaner hierarchy (ref #492)
Issues fixed :
- R2 warning panel was INSIDE the form between R2 button and footer (looked
like an extra button). Now properly separated, only shown if r2_enabled.
- No 'recommended' marker on R1. Added explicit '✓ DÉFAUT RECOMMANDÉ'
badge on the R1 button.
- 'Ton appareil : ?? (anonymisé)' showed garbage at first page load. Now
conditional : only renders if mac_hash is resolved + not '??'.
- 'Choisis ton niveau' was buried inside the terms paragraph. Now a
prominent centered label '👇 Choisis ton niveau d'analyse :' above the
form.
- Each button has a sub-desc with what it actually does + impact.
- Cleaner terms paragraph at bottom + links to /report/me/html and PDF.
|
|||
| 95e11c8118 |
fix(secubox-toolbox): R2 level switch — replace ad-hoc nft.del_validated noop with proper del_consented helper + log nft results (ref #492)
## Bug
/change-level R0 had a broken else branch :
if level in ('r1', 'r2'):
nft.add_consented(mac, ttl='24h')
else:
try:
nft.del_validated # name reference, NEVER CALLED
from . import nft as _nft
_nft._run(...)
except Exception:
pass
The 'nft.del_validated' line was a stray name reference (intended as a
name-check?) that did NOTHING. The actual delete logic was buried under
it but still ran. Worked accidentally but unclear + ugly.
Plus : nft calls (add_validated/add_consented/add_r2_banner) silently
returned False on permission errors. No way to debug from logs.
## Fix
- secubox_toolbox/nft.py : new del_consented() helper (symmetric with
add_consented).
- /change-level : flat sequence, uses del_consented for R0 downgrade,
captures rc of each nft call.
- log.info now includes 'nft: validated=True consented=True banner=False'
style status — operator can immediately see which nft op failed.
## Verified live
After R0 cycle :
- validated_macs : iPhone MAC present (extended 24h)
- consented_r2_macs : empty (correctly cleaned)
- r2_banner_macs : empty (correctly cleaned)
After R2 cycle (in service ctx with CAP_NET_ADMIN) :
- all 3 sets get the MAC, banner addon checks store level = r2 and
injects on text/html responses.
|
|||
| 5f22d601a2 |
feat(secubox-toolbox): transparency metrics — attempts counters + whitelist hits + sensitivity badge + R0 'bypass complet' label (ref #492)
## User-facing transparency improvements ### Attempts counters in dashboard Surfaced under '📈 Tentatives observées (toutes catégories)' : Total / 🔍 Inspecté / 🛡 Bypass / 🔒 Cert-pinning / 🔐 E2E / 🚫 Bloqué Honest accounting : every flow we saw, by analysis_status. Includes the Phase 4 placeholder for blocked flows (0 until enforcement is wired). ### Whitelist hits per pattern + category Surfaced under '📜 Tes bypass whitelist en détail' : - Counter per category (vendor-os, financial, e2e-messaging, etc.) - Top 8 matched patterns with hit counts (e.g. '*.apple.com · 12 hits') This is the 'accountability' angle : when we say 'we bypassed 23% of your traffic', the report now ITEMIZES exactly which patterns received those bypasses + why (category). ## Whitelist matching fix The whitelist hit counter iterated dpi_hosts only — but iPhone DPI events mostly contain IPs (cert-pinning rerouting). JA4 SNIs were ignored. Now also iterates ja4_snis so SNIs like 'smoot.apple.com' match '*.apple.com' from the baseline yaml. Verified : whitelist_hits.total = 1 (was 0), top_patterns[0] = '*.apple.com' x1, by_category = {'vendor-os': 1}. ### Sensitivity profile in dashboard Active rule_engine sensitivity profile (low/medium/high/paranoid) displayed in the transparency card with its label + description. Currently medium = '🟡 Équilibré (défaut)'. ### R0 'bypass complet' label Splash R0 button text changed from 'Accès simple' to 'Bypass complet' + subtitle 'réseau seul, ZÉRO analyse, comme un AP normal'. User feedback : 'permettre un bypass ou whitelisting complet' -> R0 already is that, just needed clear labeling. |
|||
| d726e33e33 |
feat(secubox-toolbox): /accept redirects to dashboard + /change-level endpoint + dashboard level switcher (ref #492)
## Changes
### 1. Replace success page with dashboard
POST /accept now returns 303 redirect to /report/me/html?welcome=1&level=X
instead of rendering success.html.j2. User lands DIRECTLY on the live
dashboard with all the analysis data already visible.
A welcome banner at the top of the dashboard greets the user with their
chosen level + a link to fire the Apple captive probe (closes iOS captive
sheet -> Safari ready for normal browsing).
### 2. /change-level endpoint
POST /change-level accepts level=r0/r1/r2 form field. Updates nft set
membership (validated/consented/r2_banner) + persisted clients.level.
Redirects back to /report/me/html?switched=1&level=X.
### 3. Dashboard level switcher
report-live.html.j2 has a new card '🔀 Mon niveau d'opt-in' with 3
buttons (R0/R1/R2). User can change level without going back to splash.
The 'switched' query param triggers a confirmation banner.
### 4. Pass query_args to template
report_me_html() now passes request_args (query params) + current_level
(from store) to the template — used by the welcome/switched banner +
to highlight the active level button.
## E2E flow
splash R1 click -> POST /accept (level=r1) -> 303 -> GET /report/me/html
?welcome=1&level=r1 -> dashboard with green welcome card + R1 highlighted
dashboard switch to R2 -> POST /change-level (level=r2) -> 303 -> GET
/report/me/html?switched=1&level=r2 -> dashboard with 'niveau changé'
banner + R2 highlighted + r2_banner_macs set now includes my MAC ->
banner injection + QUIC drop active.
|
|||
| 6399a6c3ee |
feat(secubox-core+toolbox): generative rule engine + sensitivity profiles + whitelist expansion + iOS captive fix (ref #492)
## 4 changes bundled ### 1. URGENT iOS 'commencer a surfer' fix The button linked to https://duckduckgo.com directly. iOS captive sheet intercepted the navigation and did nothing visible. User stuck on the success page. Fix : href -> http://captive.apple.com/hotspot-detect.html + target=_blank + JS fallback to DDG after 500ms. iOS sees the captive probe succeed, closes the captive sheet, user lands in Safari with full connectivity. ### 2. Generative rule engine (common/secubox_core/rule_engine.py) Replaces the static whitelist.py with a 3-layer decision engine : Layer 1 : STATIC whitelist-baseline.yaml + operator override Layer 2 : GENERATIVE Python predicates that auto-trust by criteria : - *.gouv.fr / service-public.fr -> trust - *.ameli.fr / cnam.fr / cpam.fr / etc. -> trust - hosts with ≥3 cert-pinning failures observed -> trust-warn (auto-learned) - high-reputation ASN (Cloudflare/Akamai/Google /Amazon/Apple/OVH) -> trust-warn Layer 3 : DEFAULT inspect (fall through) record_pinning_failure(host) called by mitm addons on TLS handshake fail -> persisted to /var/lib/secubox/toolbox/learned-rules.json -> next evaluate() returns trust-warn for that host. ### 3. Dynamic sensitivity profiles 🟢 low : block only confirmed malware (threat-intel match). DGA ≥ 90, beacon never. No false positives. 🟡 medium : balanced default. DGA ≥ 70, beacon ≥ 75. 🟠 high : strict. DGA ≥ 50, beacon ≥ 50, low-rep ASN block. 🔴 paranoid : default-deny — block anything not whitelisted. should_block(threat_intel_matches, dga_score, beaconing_score, asn_rep, is_whitelisted) returns (decision, reason). Used by scoring + (Phase 4) active blocking. Profile selectable via /etc/secubox/toolbox/rule-engine.yaml : sensitivity: low | medium | high | paranoid static_rules: [...] # operator extends baseline ### 4. Whitelist baseline expansion (47 -> 108 patterns) + Banking FR neobanks : Boursorama Banque, Hello Bank, BforBank, N26, Wise, Monzo, ING, Trade Republic + crypto (Binance, Kraken, Coinbase) + Streaming : Netflix, Disney+, Spotify, Deezer, Canal+, France.tv, Arte, YouTube + CDN, Twitch, Amazon Video + Education FR : Pronote, Ecole Directe, ENT universitaire, CNED + Health FR : Maiia, Livi + Workplace SaaS : Office365, SharePoint, Slack, Zoom, Teams, Salesforce, Atlassian, Notion, Dropbox, Box, Adobe + Gaming : Steam, Epic, EA, Battle.net, PlayStation, Xbox Live, Nintendo, Discord + DoH resolvers : Cloudflare, Google, NextDNS ## Backward compat whitelist.py kept as a thin shim — match() / is_whitelisted() delegate to rule_engine. local_store.py + inject_banner.py continue to work. ## Verified gk2 (2026-06-05) stats : static_rules=108, generative_rules=4, sensitivity=medium evaluate('impots.gouv.fr') -> trust (gov-fr generative) should_block(threat_intel_matches=1) -> True should_block(dga_score=80) -> True (≥ 70) should_block(is_whitelisted=True) -> False |
|||
| 9690837177 |
feat(secubox-toolbox): integrate NotoColorEmoji in PDF — flags + color emoji render natively (ref #492)
## Goal User feedback : 'integre NotoColorEmoji' + 'pas de flags' in PDF. Symbola alone had no glyphs for flag emojis (regional indicator PAIRS like 🇫🇷=U+1F1EB+U+1F1F7 which Symbola renders as 2 boxed letters, not as a single flag glyph). Result : PDFs showed [FR]/[US]/[DE] fallback codes instead of real flags. ## Fix Add NotoColorEmoji (CBDT/CBLC color bitmap font) to fpdf2's fallback chain. Order reversed : NotoColorEmoji FIRST, Symbola SECOND. - NotoColorEmoji has flag composites + all modern emoji as bitmaps - Symbola monochrome stays as fallback for chars Noto doesn't cover - DejaVu Sans remains primary for body text Verified : pdftotext extract shows 🇺🇸 🇨🇦 🇫🇷 directly (5 fonts embedded incl /MPDFAA+NotoColorEmoji). PDF size 51kB -> 62kB acceptable for the visual gain. ## _EMOJI_REPLACEMENTS table Reduced to a single passthrough entry (🏳 -> 🏳) since NotoColorEmoji now handles every emoji we use. Kept as scaffolding for graceful degradation if NotoColorEmoji isn't installed (debian/control already adds fonts-noto-color-emoji as a soft dep via geoipupdate path — follow-up : add as hard dep). ## debian/control Should add fonts-noto-color-emoji to Depends. Currently : fonts-dejavu-core, fonts-symbola, python3-yaml, python3-geoip2 | geoipupdate |