Compare commits
No commits in common. "master" and "webext-v0.1.0" have entirely different histories.
master
...
webext-v0.
|
|
@ -3,673 +3,6 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-06-27 — LAN standardisé 192.168.10.0/24 + c3box/gk2 live Freebox + bump 1.10.0 (#760)
|
||||
|
||||
Session terrain "c3box derrière Freebox" : la LAN SecuBox par défaut (`br-lan 192.168.1.1/24`)
|
||||
entrait en collision avec la LAN d'un routeur opérateur courant (Freebox/Livebox en
|
||||
`192.168.1.0/24`). En aval d'une Freebox, le WAN DHCP et la LAN se retrouvaient sur le **même
|
||||
sous-réseau** → route dupliquée, ARP ambigu, IP de management injoignable.
|
||||
|
||||
### A. Constat live + remédiation immédiate
|
||||
- **c3box** (second MOCHAbin) derrière Freebox : WAN `eth2=192.168.1.94` (bail Freebox) +
|
||||
`br-lan=192.168.1.1/24` → `.94` injoignable depuis le LAN. Corrigé live : `br-lan → 192.168.10.1/24`.
|
||||
SSH root activé, webadmin `https://192.168.1.94/` OK, `/dev/sda1` (931 G) monté sur `/data`
|
||||
(style gk2 : UUID + nofail), partition eMMC retirée (`emmc-data`).
|
||||
- **gk2** (live PoC) : uplink déplacé de `lan0` (DSA) vers le port cuivre WAN `eth2` ; netplan
|
||||
réparé via **série** (gk2 hors-réseau le temps du switch) → `eth2 dhcp4: true`, `lan0` dépouillé.
|
||||
Bail Freebox réservé sur le MAC eth2 `f0:ad:4e:27:88:9b` → gk2 reprend `192.168.1.200`. Persisté.
|
||||
|
||||
### B. Standardisation source (LAN = 192.168.10.0/24, gw .10.1) — 17 fichiers
|
||||
- Netplans board : mochabin, espressobin-v7, espressobin-ultra, x64-vm, x64-live (`br-lan`),
|
||||
+ unification VM vm-x64/vm-arm64 (`192.168.100.1 → 192.168.10.1`).
|
||||
- Générateurs de netplan : `secubox-netmodes`, `secubox-hub` (preview), `secubox-net-detect`.
|
||||
- dnsmasq (`espressobin-v7.conf`) : `dhcp-range` + `option:router` + `option:dns-server`.
|
||||
- Scripts live-usb (mochabin/ebin) + SAN des certs auto-signés (`firstboot`, `build-image`,
|
||||
`build-rpi-usb`, `build-live-usb`) → `IP:192.168.10.1`.
|
||||
- **Hors scope (intacts)** : `192.168.255.1` (whitelist mgmt/trusted-proxy WAF/mail/wg/mitm),
|
||||
listes `GATEWAYS` de sonde WAN, exemples remote-ui/round + tests.
|
||||
|
||||
### C. Release
|
||||
- Bump mineur (« medium ») **1.9.0 → 1.10.0** : `build-image.sh`, `build-live-usb.sh`,
|
||||
`build-ebin-live-usb.sh`, `build-rpi-usb.sh` (mochabin-live reste sur sa piste 2.0.0).
|
||||
- Artefacts amd64 (x64) reconstruits depuis cette base.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-27 — Netboot live PROUVÉ + première install SecuBox Debian sur c3box (second MOCHAbin) (#748 #737)
|
||||
|
||||
Grande session hardware : netboot gk2→c3box validé de bout en bout, premier SecuBox Debian installé
|
||||
sur un vrai MOCHAbin, et le blocage U-Boot qui empêche #748 de fermer est formellement documenté.
|
||||
|
||||
### A. Netboot gk2 → c3box : validé en prod
|
||||
|
||||
- **c3box** (second MOCHAbin, Armada 7040) a booté l'installeur SecuBox Debian servi par gk2 via
|
||||
TFTP : factory U-Boot 2020.10 → `tftpboot Image/dtb/initrd` → `booti` → rescue shell installeur,
|
||||
kernel custom 6.12.85 #5secubox. Le FIT signé (49 Mo) était servi en HTTP sur `:8099`.
|
||||
- Le long détour cabling était une impasse LAB (prouvé via gk2 bridge-FDB + test DHCP) — aucun
|
||||
bug logiciel.
|
||||
- **Learnings opérationnels réutilisables** (documentés dans `wiki/Netboot-Install.md`) :
|
||||
- Factory U-Boot 2020.10 s'interrompt sur **Enter** (pas Ctrl-C), `bootdelay=2`.
|
||||
- Son env n'est PAS dans SPI mtd2 (env étranger fossile) → `fw_setenv` depuis Linux n'a aucun
|
||||
effet ; seule la config U-Boot interne compte.
|
||||
- Seul le port cuivre RJ45 unique = `mvpp2-2` est bootable par le factory U-Boot (les 4 ports
|
||||
switch nécessitent le driver MV88E6XXX DSA, absent au boot).
|
||||
- Kernel load à `0x02080000` = adresse mémoire réservée → crash immédiat ; utiliser `0x0a000000`.
|
||||
- `setenv tftpblocksize 1468` pour TFTP rapide.
|
||||
|
||||
### B. #748 enhanced Tow-Boot (HTTP/wget bootloader) — DIFFÉRÉ, bloquant documenté
|
||||
|
||||
Branche `feature/748-enhanced-tow-boot-http-netboot-serial-fl` (stackée sur #737) :
|
||||
spec+plan (`docs/superpowers/`), Kconfig Tow-Boot, `build-uboot-overlay.sh --tow-boot`,
|
||||
plan serial-flasher, CI `.github/workflows/build-tow-boot.yml` (push-triggered).
|
||||
|
||||
**Bloquant dur (ciseau)** : le board MOCHAbin n'existe que dans le fork U-Boot 2022.07 de
|
||||
Tow-Boot (pas de `wget`) ; `wget` n'existe que dans U-Boot stock ≥2023.07 (pas de board
|
||||
mochabin/DTS). Bump à stock 2023.07 = `wget` compile mais build sans DTS. Pour débloquer :
|
||||
backporter wget/TCP dans le fork Tow-Boot 2022.07, OU porter le board mochabin vers mainline
|
||||
≥2023.07. Pas un tweak de config.
|
||||
|
||||
### C. PREMIÈRE INSTALL — c3box → SecuBox Debian (la headline)
|
||||
|
||||
- **Image** : artefact CI `secubox-mochabin-bookworm` (run 27426515472, 1,8 Go gzip / 8,0 Gio
|
||||
décompressé), téléchargée sur gk2 `/data`, SHA256SUMS vérifié.
|
||||
- **Signature** : clé `secubox-netboot.key` de gk2. Vérifié : cette clé FIT == `netboot-image.pub`
|
||||
embarquée dans l'installeur (modulus match + roundtrip sign/verify). `sbx.img.gz` + `.sig`
|
||||
publiés dans le root HTTP netboot, servis sur `:8099` (symlink depuis `/data`).
|
||||
- **Install automatisé depuis le rescue shell** :
|
||||
`wget sbx.img.gz` (en RAM, c3box a 8 Go) →
|
||||
`openssl dgst -verify` contre `netboot-image.pub` (résultat : Verified OK) →
|
||||
`gunzip | dd of=/dev/mmcblk0 bs=4M conv=fsync` (8 Gio, progression 32→62→94→100%) → sync.
|
||||
- **c3box démarre SecuBox Debian v1.9.0** — hostname `secubox-mochabin`, kernel Debian
|
||||
6.1.0-47-arm64, stack complète : secuboxd, hub, grafana, zigbee, mqtt, authelia,
|
||||
sentinel/rogue-BTS (layers WALL+MIND). Creds root/secubox, Web UI `:9443`.
|
||||
- **Fix auto-boot persistant** : l'image utilise `extlinux.conf` à `0x02080000` (adresse réservée
|
||||
factory U-Boot → reset immédiat) et ne livre pas de `boot.scr` compilé. Construit
|
||||
`/boot/boot.scr` (kernel@`0x0a000000`, initrd@`0x10000000`, `console=ttyS0` + earlycon,
|
||||
`root=LABEL=rootfs`) : le factory U-Boot charge `boot.scr` depuis mmc et démarre Debian sans
|
||||
intervention. **VÉRIFIÉ** : reboot sans intervention → login Debian.
|
||||
- **Layout eMMC installé** : GPT p1=boot (FAT, `/boot`) p2=ROOT (`/`) p3=DATA. c3box était
|
||||
OpenWrt ; eMMC écrasé (install RAM-only, pas de risque sur l'OS tournant avant le `dd`).
|
||||
- **Rig netboot temporaire gk2 encore actif** : `lan1=192.168.77.1/24`, dnsmasq test (DHCP) sur
|
||||
`lan1`, `nft iif lan1 accept`, nginx boot-vhost extra listen `192.168.77.1:8099`.
|
||||
|
||||
## 2026-06-24 (cont.) — R4 analyst mode: MITM-everything + media reverse-catcher + clone (#736)
|
||||
|
||||
New "R4" doctrine — visibility over performance. Delivered + live on gk2:
|
||||
- **Splice flip** — `tls-splice-seed.conf` reduced from a media-CDN perf list to
|
||||
breakers-only (`api.anthropic.com`); splice now applied ONLY where MITM provably
|
||||
breaks (cert pinning). Banner reaches every page; catcher sees media URLs. Live:
|
||||
learned splices cleared, autolearn gated (`tls_splice=off`).
|
||||
- **sbxmitm media reverse-catcher** (`cmd/sbxmitm/mediacatch.go`, toolbox-ng 0.1.20)
|
||||
— 2xx MITM'd flows → cloneable media URLs (HLS/DASH manifests, direct A/V,
|
||||
googlevideo videoplayback) appended to `/run/secubox/media-catch.jsonl` (URLs
|
||||
only, deduped, atomic, fail-open). `--media-catch` default on; worker unit
|
||||
`ReadWritePaths=/run/secubox`.
|
||||
- **mediaflow Discovered Media + Clone** (2.1.0) — `/discovered`, `/clone`
|
||||
(yt-dlp→ffmpeg queue, lazy worker for the aggregator), `/library`,
|
||||
`/download/{id}`, DELETE; dashboard cards. Verified: HLS caught → ffmpeg →
|
||||
464 MiB mp4 in library. yt-dlp installed.
|
||||
- Also fixed the empty mediaflow dashboard (2.0.2 contract + 2.0.3 cumulative
|
||||
services): cards/streams live, Top Media Services from DPI cumulative store.
|
||||
KEY: dashboard routes via the **aggregator** (in-process import) — restart
|
||||
`secubox-aggregator` to pick up mediaflow code changes.
|
||||
- Phase 4 done — R4 button added to the banner topbar (R0..R4) + set-level + by-MAC
|
||||
validation + analytics buckets; gated to the wg path like R3 (secubox-toolbox 2.7.20).
|
||||
- yt-dlp upgraded 2023.03.04 → 2026.06.09 (standalone binary; YouTube works).
|
||||
- Recos: catcher now captures YouTube watch **pages** (kind=page, toolbox-ng 0.1.22);
|
||||
Discovered Media persisted off tmpfs into a durable capped store (mediaflow 2.1.1);
|
||||
yt-dlp packaged (Recommends + weekly refresh timer + postinst).
|
||||
- **Catch-log ownership bug** — `/run/secubox/media-catch.jsonl` was created
|
||||
`secubox`-owned while the worker runs as `secubox-toolbox`, so O_APPEND failed
|
||||
silently → nothing captured. Fixed with a tmpfiles.d entry pre-creating it owned
|
||||
by the writer every boot (zz-secubox-toolbox-ng.conf). Live: rm + worker recreate.
|
||||
|
||||
## 2026-06-24 (cont.) — Banner on nonce-CSP sites + Claude API splice + YouTube unblock (#728)
|
||||
|
||||
Three distinct root causes behind "no banner on youtube / news", fixed in order:
|
||||
|
||||
1. **Trusted Types** (0.1.17) — `require-trusted-types-for` blocked DOM injection. Stripped.
|
||||
2. **Nonce-based CSP** (0.1.18) — the banner is *inlined* (service-worker-proof), but a CSP
|
||||
nonce/hash makes `'unsafe-inline'` IGNORED → the bare inline `<script>` was silently
|
||||
blocked. `relaxCSPForLoader` now **borrows the page's own nonce** and stamps it on the
|
||||
injected `<script nonce=…>` (surgical: page CSP/nonces/hashes untouched), falling back to
|
||||
forcing `unsafe-inline` (drop nonce/hash/strict-dynamic) only when there's no nonce.
|
||||
Nonce validated to base64 charset (attribute-breakout guard). Threaded nonce through
|
||||
injectIntoBody → injectHTML → injectInlineBanner. Tests rewritten for inline semantics.
|
||||
3. **YouTube wholly blocked** (runtime) — autolearn false-positive put `youtube.com` in
|
||||
`/var/lib/secubox/toolbox/learned-trackers.txt` → `Decide()` returned `block` (204) →
|
||||
page never loaded. Removed from learned + added to `ad-allowlist.txt` (hot-reloaded).
|
||||
Latent-bug tracker: **#735** (autolearn must not block apex/first-party nav targets).
|
||||
|
||||
**Claude API splice** (user request) — `api.anthropic.com` added to `tls-splice-seed.conf`
|
||||
(+ live seed): cert-pinned Claude API/SDK clients reject the MITM CA, so pass them through;
|
||||
`claude.ai` web stays MITM'd (browser trusts the CA → still gets the banner).
|
||||
|
||||
Verified end-to-end on gk2: YouTube 200 + banner nonce == page nonce; lemonde/lefigaro
|
||||
banner via unsafe-inline fallback. DPI confirmed healthy — collector writes to
|
||||
`/var/lib/secubox/dpi/` (state.json/cumulative.json fresh), `/exfil` returns categorized
|
||||
flows; the earlier "empty" was me checking the wrong paths (`/run/secubox/dpi`).
|
||||
|
||||
## 2026-06-24 — DPI YouTube bannering: strip Trusted Types CSP (#728)
|
||||
|
||||
- **Root cause** — YouTube serves a standalone `Content-Security-Policy:
|
||||
require-trusted-types-for 'script'` header. sbxmitm's `relaxCSPForLoader` already
|
||||
relaxed `script-src` (drop `strict-dynamic`, add `'self'`/`'unsafe-inline'`) so the
|
||||
banner loader runs, but Trusted Types still blocked the banner's DOM injection →
|
||||
banner silently never mounted on YouTube.
|
||||
- **Fix** (`cmd/sbxmitm/csp.go`, toolbox-ng 0.1.17) — drop `require-trusted-types-for`
|
||||
and `trusted-types` directives during the relax; omit the resulting empty CSP header
|
||||
line. Local Go unit tests cover both the relax and the empty-header drop.
|
||||
- **DPI capture half** — collector `state.json` was stale (frozen 09:44); restarted
|
||||
`secubox-dpi-flowcap` → fresh windows, YouTube/media flows now visible in mediaflow.
|
||||
- Deployed to gk2; R3 workers `secubox-toolbox-ng-worker@1..4` restarted on 0.1.17.
|
||||
- Filed for later: #729 wireguard peers/tabs, #730 yacy, #731 lyrion, #732 magicmirror,
|
||||
#733 firewall dashboard misreport, #734 webui.conf hardcoded-route cleanup.
|
||||
|
||||
## 2026-06-22 — DPI exfil engine + Netrunner report (HTML+PDF) + sbxmitm fixes
|
||||
|
||||
Big session: full per-device DPI exfiltration pipeline, the kbin report reborn as a
|
||||
cyberpunk-netrunner character sheet, and two live-ops fixes on the Go MITM engine.
|
||||
All PRs merged to master and deployed live on gk2.
|
||||
|
||||
### DPI — per-device cloud-exfiltration (#687, secubox-dpi 1.0.5 → 1.1.2)
|
||||
- **Phase 1** nDPI flow-DPI on `wg-toolbox` (ndpiReader, ~1% CPU on the Armada).
|
||||
- **Phase 2** Go collector (`secubox-dpi-collector`, pure stdlib, arm64): attributes
|
||||
flows to devices via `sha256(wg_pubkey)[:16]`, classifies SNI into nDPI-style
|
||||
**categories** (cloud/filehost/messaging/ai/media/game/social/adult), fires exfil
|
||||
scenarios (`exfil_volume`, `new_cloud`, `beaconing`, `unclassified_external`).
|
||||
Producer = `secubox-dpi-flowcap` (60s windows) → `GET /api/v1/dpi/exfil`.
|
||||
- **Dashboard** (#693/#695): "Cloud Exfiltration Watch" panel + stat cards + all list
|
||||
cards repointed off the inactive netifyd to the live exfil engine.
|
||||
- **#692** beaconing tuned to a C2-plausible cadence (1s–1h, CV≤0.25, external).
|
||||
- **#705 cumulative 7d** — `cumulative.json` so the report shows history, not just the
|
||||
last 60s window (was: idle device → all zeros).
|
||||
- **Packaged** `secubox-dpi 1.1.x` (arch arm64, Go built in debian/rules offline,
|
||||
flowcap auto-enabled, `Depends: libndpi-bin`).
|
||||
|
||||
### kbin report — Cyberpunk-Netrunner character sheet (#707, HTML + PDF)
|
||||
- **#699** report tabs (Pistage / DPI-Exfil / Overall) with donut charts.
|
||||
- **#701/#703** DPI stats + visual donut charts in the PDF (mitm/certs/ads/dpi).
|
||||
- **#707** persona sheet: class+emoji from the request UA (live device), level=R3 for
|
||||
wg peers, ICE/Exposition bars, XP, 4 pip-bar CARACTÉRISTIQUES, Inventaire, Bestiaire,
|
||||
Quêtes — HTML neon + PDF `_persona_block`.
|
||||
- **#709** carto hub map + emoji tables (Traceurs/Pays/DPI) in the PDF.
|
||||
- **#711/#712** "En un coup d'œil" added to the PDF.
|
||||
- **#714** charts switched to **matplotlib PNG** embeds (fpdf2 vector donuts were blank
|
||||
in iOS/Chrome viewers).
|
||||
- **#716** donut grid → ONE combined 2×2 image (was spilling each donut/legend onto its
|
||||
own page → 24 pages). Report back to a clean 4 pages. User: "report parfait".
|
||||
|
||||
### sbxmitm (Go MITM engine, #662 line)
|
||||
- **#689** forged leaf cert TTL **24h → 365d** — root cause of recurring "certificat
|
||||
expiré" on clients (cache never evicts; 24h leaves expired daily). Interception kept.
|
||||
- **#697** stop truncating responses >8MiB — `streamResponse()` streams non-injected
|
||||
bodies verbatim; large **Gmail** messages/attachments rendered again over R3.
|
||||
- **#688** own-domain splice approach REJECTED (decision: intercept all vhosts) — reverted.
|
||||
|
||||
### Ops notes
|
||||
- Surf-break incident: R3 mitm CA rotated 2026-06-05 → clients must re-import the CA root
|
||||
(the "expired cert" was client-side trust, not the board).
|
||||
- R3 engine is the Go `sbxmitm` (`secubox-toolbox-ng-worker@1..4`, 10.99.1.1:8091-8094)
|
||||
— NOT the Python mitm; restart THOSE for R3 changes.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-20 — kbin Tor shipped + client releases + ad-block/mitm hardening
|
||||
|
||||
- **#683 MERGED (PR #684)** — kbin Tor egress quick-switch (switch + nft owner-match
|
||||
tunnel, own-services exemption, reconciler+timer), dashboard/landing/banner metrics
|
||||
fixes, 🧅 indicators (banner/webext/APK), APK persistent WG identity, landing+report
|
||||
**redesign** (verdict gauge + donut/bars + collapsible details). Live on gk2; Tor armed.
|
||||
- **Client releases served from kbin**: `android-v0.4.0` (Latest) + `webext-v0.1.5`
|
||||
published by CI; pinned webext tag bumped; board fetch-helpers pull them →
|
||||
/wg/toolbox.apk (0.4.0) + /wg/toolbox.xpi (0.1.5). toolbox 2.7.12.
|
||||
- **#685 ad-learner hardened (2.7.13)** — NEVER_LEARN guard (Google/CDN/fonts/captcha/
|
||||
auth/payment), AD_MIN_SITES 1→2, prune existing. Root cause of euronews breakage:
|
||||
the learner had 204'd `www.google.com` → broke reCAPTCHA/consent. Also allowlisted
|
||||
www.google.com/.fr live.
|
||||
- **mitm-wg stream_large_bodies=1m (2.7.14)** — large binary downloads (APK, CA) were
|
||||
corrupted ONLY through the R3 tunnel (HTTP/2 buffer/reframe); now passed verbatim.
|
||||
- **OPEN [#686]** — android-toolbox non-root flow broken (CA auto-install needs root,
|
||||
WG handoff → Play Store, tunnel not detected). Needs on-device dev/testing; rooted-vs-
|
||||
non-rooted decision pending. #685 signing was a red herring (corrupt = mitm buffering).
|
||||
|
||||
## 2026-06-19 — kbin Tor egress quick-switch implemented DARK (#683, ToolBoX 2.7.1)
|
||||
|
||||
- **Switch + tunnel** for routing kbin surfing through Tor, shipped **default-OFF /
|
||||
fail-closed** on `feature/683`. Reuses existing secubox components per the user ask.
|
||||
- **Transport decision (USER): torify the MITM egress.** nft owner-match on the
|
||||
`secubox-toolbox` (mitm-wg) uid → Tor TransPort 9040 / DNSPort 5353. Clients →
|
||||
TPROXY → mitm decrypts/ad-blocks/poisons/banners/re-encrypts → exits via Tor.
|
||||
**Inspection fully preserved**; only the exit IP + network identity change. (Rejected:
|
||||
SOCKS5 Go-core dialer = blocked on #662; transparent client torify = breaks inspection.)
|
||||
- **Switch**: `filters.json` flags `tor_mode`/`tor_preset`; API (kbin-gated, admin.gk2
|
||||
only for actions) `GET/POST /admin/tor/{state,on,off,newnym,check-leaks}`; 🧅 WebUI tab
|
||||
(badge bootstrap/circuits/exit-IP, toggle, NEWNYM, SOCKS leak probe). `tor_ctl.py`
|
||||
reuses secubox-tor's control-port code — no cross-service JWT.
|
||||
- **Tunnel arms via reconciler**: root, path-triggered (`secubox-toolbox-tor.path`
|
||||
watches filters.json) → portal stays `NoNewPrivileges=true`, no sudo. nft loaded
|
||||
BEFORE tor (no clearnet window); IPv6 worker egress dropped (no v6 leak); prerm
|
||||
disarms on real removal (not upgrade). Depends jq; Recommends tor + python3-socksio;
|
||||
postinst adds secubox-toolbox to debian-tor group.
|
||||
- **Verified**: 166 toolbox tests green (10 new), nft syntax valid (user-resolve only),
|
||||
maintainer scripts `sh -n` clean, license headers OK, changelog parses 2.7.1.
|
||||
- **Granularity = global kbin Tor mode** (owner-match can't be per-client). Per-client
|
||||
(WG-hash) Tor tracked under #662 (Go-core SOCKS5 dialer). NOT yet flipped/deployed —
|
||||
needs soak + off-board leak test + tls_splice(#649)-OFF before arming.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-19 — kbin milestone: ToolBoX 2.7.0 (middle release) + Tor chapter staged (#683)
|
||||
|
||||
- **End-of-session checkpoint** — docs + positioning + version, no runtime behaviour change.
|
||||
- **`secubox-toolbox` 2.6.59 → 2.7.0** (middle release) — caps the 2.6.x line
|
||||
(ad-intelligence / Anti-Track v2 / anti-bot uTLS #662) and opens the **kbin** chapter:
|
||||
kbin (`kbin.gk2.secubox.in`, the public ToolBoX portal) framed as the *first tool of the
|
||||
CyberMind Swiss-army cyber kit* — transparent performance, full-encrypted MITM inspection,
|
||||
ad poison/smog injection, adware-ban transparency banner, safe browsing.
|
||||
- **Docs** — new wiki use-case `docs/wiki/Kbin-Toolbox.md`, `docs/FAQ-KBIN-TOR.md`,
|
||||
README positioning blurb.
|
||||
- **Plan #683 (issue + spec)** — kbin **Tor endpoint**: a quick-switch re-routing consenting
|
||||
client surfing through Tor (outbound egress, pseudo-network) so the kbin exit is anonymized.
|
||||
Spec `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`. Invariants:
|
||||
inspection preserved (Tor after the forging core), fail-closed, opt-in/default-OFF, no DNS
|
||||
leak, CSPN audit-logged. Opposite direction of `secubox-exposure` (inbound hidden services);
|
||||
reuses its Tor control. Depends on the #662 Go core for the preferred SOCKS5-dialer transport.
|
||||
- **Caveat recorded** — Tor mode must force `tls_splice` (#649) OFF per-client or asset flows
|
||||
leak the real IP.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-19 — #662 anti-bot: Chrome TLS fingerprint (uTLS) — defeat DataDome without splice (PR #674)
|
||||
|
||||
- lemonde.fr (DataDome) blocked R3 navigation at the 2nd level: the engine re-origined
|
||||
upstream TLS with a Go JA3/JA4 → flagged as bot. Splice rejected (don't exempt a
|
||||
tracking site). Fix: upstream transport now presents a real **Chrome** fingerprint
|
||||
via **uTLS HelloChrome_Auto + h2-over-uTLS**. Verified live: JA4
|
||||
`t13d1516h2_8daaf6152771_02713d6af862` (Chrome), was Go.
|
||||
- **Cert verification preserved** (manual verifyUConn: system roots + intermediates +
|
||||
hostname; adversarially tested). Stopped the Accept-Encoding downgrade (was a tell) +
|
||||
added brotli/zstd decode-inject-reencode. H1 response-header timeout.
|
||||
- First vendored deps (utls/brotli/zstd/x-net, pure-Go), offline arm64 via -mod=vendor.
|
||||
Canary 1 worker → verified Chrome FP + cert chains + ad-block + banner → widened to 4.
|
||||
- Caveat: DataDome also fingerprints HTTP/2 + behaviour — uTLS helps strongly, not a
|
||||
100% guarantee. Browser test is the real confirmation.
|
||||
|
||||
## 2026-06-19 — #662 post-cutover restore: ad-block metrics + popup CSS (PR #673)
|
||||
|
||||
- **Found by verification**: the cutover ported the 204-block but NOT ad_ghost's
|
||||
metrics recording (frozen since 2026-06-18 18:59) nor its cosmetic/popup-hiding CSS
|
||||
(popups returned — they're 1st-party DOM, never touched by host-204).
|
||||
- **Metrics**: Go aggregates blocks in-memory (per ad_host/site + per mac_hash), flushes
|
||||
every 10s to a new portal `POST /__toolbox/ad-event` (unauth R3-perimeter, body-bounded,
|
||||
never 500s) → SQLite store → #ads dashboard live again (total_blocked rising).
|
||||
- **Popups**: Go injects `<style id="sbx-ghost-style">` on R3 HTML (wg-gated, idempotent,
|
||||
on the gzip path with the banner) — ports `_COSMETIC` + ad-specific popup tokens
|
||||
(interstitial/ad-overlay/popup-ad/popunder/exit-intent), conservative (no bare
|
||||
modal/popup/overlay, regression-tested). Verified live on the R3 path.
|
||||
- toolbox-ng 0.1.5 deployed (rolling restart) + portal api.py hot-deployed (drift closed
|
||||
at next .deb build). Portal uvicorn boot ~14s.
|
||||
|
||||
## 2026-06-18 — #662 Phase 7: Python R3 engine DECOMMISSIONED + nft persistence
|
||||
|
||||
- **nft persistence** (master `eea46326`): the boot re-apply source is the drop-in
|
||||
`/etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft` (loaded by nftables.service). Edited
|
||||
it `808x→809x` (live already 809x → zero disruption), `nft -c -f` validated reboot-safe;
|
||||
patched the repo source `packages/secubox-toolbox/nftables.d/secubox-toolbox-wg-fanout.nft`.
|
||||
- **Python decommissioned**: `disable --now secubox-toolbox-mitm-wg-worker@{1..4}` +
|
||||
`-mitm-wg-dynreload.path` → 8081-8084 free, **~240M RAM freed**. Units kept (disabled)
|
||||
for emergency rollback. **Kept** `secubox-toolbox-mitm.service` (R2 captive-AP mitm on
|
||||
10.99.0.1:8080 — a different path; the cutover was R3-only). Also pointed the board's
|
||||
`/usr/share/.../secubox-toolbox-wg-fanout.nft` → 809x so a postinst re-run can't revert
|
||||
to dead ports.
|
||||
- **Verified self-sufficient with Python gone**: banner injects on gzip HTML, ads 204,
|
||||
redirects relayed 301.
|
||||
- Deliberately did NOT rebuild+reinstall the secubox-toolbox .deb (portal-restart blip +
|
||||
board-wide nft reload, gratuitous) — repo source is 809x, the next natural build closes
|
||||
the installed-payload drift. **#662 epic complete: Go engine sole R3 MITM, fast, ~64MB
|
||||
vs ~280-470MB, persistent, ad-block + banner + redirects all correct.**
|
||||
|
||||
## 2026-06-18 — #662 R3 CUTOVER to the Go MITM engine (PR #670) — LIVE + banner ported
|
||||
|
||||
- **Cutover executed and live.** The Go engine now serves **100% of R3 traffic**,
|
||||
replacing the Python mitmproxy workers. Found + fixed 4 blockers that made the dark
|
||||
package unable to serve the live path: (1) it forged with the wrong CA (ca-wg "WG CA"
|
||||
vs the "R3 CA" clients trust) → now uses the mitmproxy confdir bundle; (2) root-only
|
||||
key vs non-root user → R3 CA bundle is group-readable; (3) bound 127.0.0.1 vs the
|
||||
10.99.1.1 DNAT target → now binds 10.99.1.1; (4) ran CONNECT vs transparent → now
|
||||
`--transparent`. `loadCA` scans PEM blocks by type (combined cert+key bundle).
|
||||
- **Validated on real arm64 hardware** then rolled out gated: localhost forge against
|
||||
the real R3 CA → scoped-DNAT transparent capture → **canary slot 3 (~25%, dead-man
|
||||
armed)** → **widen to 100%**. At 100%: 0 restarts, 0 errors, ~64MB total
|
||||
(vs Python ~280-470MB), even round-robin, 142 distinct SNIs/75s.
|
||||
- **Banner ported** (the one regression the user caught — "no more banner but fast").
|
||||
Go now injects the real loader `<script src="/__toolbox/loader.js" data-mh=.. data-wg=..>`
|
||||
(guard-idempotent, R3 wg flag, mac_hash identity) and reverse-proxies
|
||||
`/__toolbox/loader.js`+`/__toolbox/bundle` to the portal (127.0.0.1:8088, fail-open),
|
||||
keeping bundle/level logic in Python. Verified live: loader injected + assets 200.
|
||||
- **Rollback** = one `nft replace` (Python workers kept warm). **Persistence gap**: the
|
||||
nft flip is a live edit, not yet in the drift-managed generator → reboot safely falls
|
||||
back to Python (workers enabled, banner intact). Phase 7 (decommission Python +
|
||||
persist nft) deferred to a soak'd follow-up.
|
||||
|
||||
## 2026-06-18 — #662 MITM engine migration: P5-prep + P6-prep (PRs #668, #669, all DARK)
|
||||
|
||||
- **P5-prep (PR #668).** Wired the ported `Decide`+jar into the Go engine's request/
|
||||
response handlers: `handleConnect` runs allow/splice/block/mitm; `anonymizeRequest`
|
||||
(strip operator/re-id headers + DNT/GPC) on every MITM'd flow; cookie-poison gated
|
||||
to mitm+tracker only (never allow/own-infra; fail-closed-to-clean; benign cookies +
|
||||
Set-Cookie attrs preserved). New `secubox-toolbox-ng` debian pkg builds an arm64
|
||||
`.deb` shipping `/usr/sbin/sbxmitm` + a **DISABLED** `worker@.service` on `:809%i`
|
||||
(no enable/start, no nft). 22 Go tests, reviewed APPROVED.
|
||||
- **P6-prep (PR #669).** No-traffic build-out of the live transparent path, still DARK.
|
||||
`machash.go` ports `mac_hash_of`/`_wg_hash_of` (WG peers → `sha256(pubkey)[:16]`,
|
||||
mtime-cached, fail-open) wired into `clientHashFromConn`, cross-engine parity vs
|
||||
Python (anti-rig verified). Transparent `SO_ORIGINAL_DST` accept (`--transparent`,
|
||||
default off): peeks ClientHello SNI WITHOUT decrypting → Decide → **splice = true raw
|
||||
passthrough** (never `tls.Server`) / else forge via replayable `prefixConn`; upstream
|
||||
TLS verifies by SNI, pins captured ip:port. Two-stage review caught + fixed a
|
||||
splice-decrypt defect. Builds linux/arm64+amd64+darwin, vet clean, race green, Python
|
||||
parity 10 passed. CONNECT path + poison gate byte-unchanged.
|
||||
- **Engine now functionally complete + packaged, entirely DARK.** Remaining work =
|
||||
the production DEPLOYMENT phases (shadow → cutover → decommission), which touch live
|
||||
R3 traffic and are deferred to a deliberate watched session — NOT chained off "go".
|
||||
|
||||
## 2026-06-18 — #656 Ad Intelligence (PR #657, toolbox 2.6.56) + splice reverted
|
||||
|
||||
- **Ad Intelligence — learn/act/measure.** `ad_ghost` now records every
|
||||
block/silent per (ad_host, site=registrable(Referer), action) into a new
|
||||
`ad_block_stats` store (in-memory dicts, bg-thread flush — no SQLite on the
|
||||
proxy hot path), exposed via `GET /admin/ad-stats` + a new **#ads dashboard
|
||||
tab** (top ad hosts, ads-blocked-per-site, action split, KB saved). Aggressive
|
||||
learning: 3rd-party ad-shape requests captured as `ad_candidates`; autolearn
|
||||
`_ad_feed` promotes hosts on ≥AD_MIN_SITES (default 1) distinct sites into the
|
||||
204'd blocklist. Safety (inverts the splice mistake — learning to BLOCK is
|
||||
reversible): `ad-allowlist.txt` always wins, `ad_learn` toggle, every block
|
||||
visible in metrics, no IP-drop, no CSP weakening. 115 tests green; deployed +
|
||||
verified (/admin/ad-stats 200, metrics flowing, ad_ghost intact).
|
||||
- **Splice (#649/#651) REVERTED to off.** `tls_splice=on` bypassed the whole
|
||||
addon chain → autolearn promoted telemetry/tracker hosts (datadog/MS/newsroom)
|
||||
to splice → ad_ghost/anti-track bypassed → ads returned. Flipped `tls_splice=off`
|
||||
(full MITM, ad-blocking restored). Splice perf vs ad-blocking is a fundamental
|
||||
conflict; needs media-only-no-learn rework before any re-enable.
|
||||
- **Banner #653 reverted** (async loader can't read currentScript → inline-bundle
|
||||
was dead code; setupReassert regressed the banner). Board on 2.6.55-equivalent
|
||||
banner. The strict-CSP/SPA banner gap (YouTube) is the browser-extension's job
|
||||
(webext content-script WIP on `feature/655`, paused).
|
||||
|
||||
## 2026-06-18 — #649 selective SNI-splice (Lever A) shipped dark (PR #650, toolbox 2.6.54)
|
||||
|
||||
- **Architecture decision.** Asked "do we need a full mitm for R3 HTTPS?" Answer:
|
||||
outbound HTTPS interception intrinsically needs per-host cert forging (the
|
||||
WAF/own-cert analogy doesn't transfer) — so we keep a forging MITM but only
|
||||
decrypt flows we'd actually modify. Plan = A-then-B: **A** = selective
|
||||
SNI-splice (this), **B** = Go/Rust core (strategic, later). WAF deferred.
|
||||
- **Lever A.** New `tls_splice` addon (first in the mitm-wg chain) decides at the
|
||||
TLS ClientHello, from the SNI alone, whether to MITM or **splice** (raw
|
||||
passthrough — no forge/decrypt/parse/16-addons). Policy: curated media-only seed
|
||||
(googlevideo/ytimg/fbcdn/twimg/scdn…, deliberately NOT generic CDN edges) ∪
|
||||
autolearn-promoted never-HTML hosts (`splice_host_obs` table, ≥20 obs,
|
||||
html_hits==0). Never splices trackers/fortknox/no-SNI/media_cache-on. Learning
|
||||
obs recorded off the event loop (bg thread), only for undecided hosts.
|
||||
- **Dark-launch.** Ships `tls_splice=observe` (classify + log would-splice, still
|
||||
MITM — zero behavior change); `on` flip is post-soak; `off` kill-switch.
|
||||
- **Built TDD** (7 tasks, 102 tests), two-stage reviews per task + whole-branch
|
||||
review (APPROVED; closed a hot-path sync-SQLite issue → bg-thread offload, and a
|
||||
fortknox-WebUI never-set refresh gap). **Deployed gk2 2.6.54**, rolling restart
|
||||
of the 4 workers, addon loads clean, 0 runtime errors, dark default confirmed.
|
||||
Next: soak → review → flip `on`.
|
||||
|
||||
## 2026-06-18 — #623 systemic shared-parent clobber resolved at source (PR #648)
|
||||
|
||||
- **Root cause corrected.** The recurring `/var/{lib,log,cache,…}/secubox` parent
|
||||
clobber was NOT the `install -d -m 0750 /parent/leaf` leaf form (empirically
|
||||
proven harmless: GNU `install -d -m` modes only the final component). It was the
|
||||
scaffold boilerplate `install -d -m 750 /var/lib/secubox` + `/run/secubox` (BARE
|
||||
parents) in ~56 module postinsts — written `-m 750` (3-digit), which is why prior
|
||||
greps/sweeps (#511/#627/#631) missed it.
|
||||
- **Source-wide fix.** Scripted rewrite of all bare-parent targets → `/run/secubox`
|
||||
1777 root:root, `/var/lib|log|cache|etc|usr/share/secubox` 0755; 6 multi-arg
|
||||
lines split per-parent (4 were setting `/var/lib/secubox` world-writable 1777 —
|
||||
a security regression); 3 `chmod 750 /var/log/secubox` (soc-gateway/soc-agent/
|
||||
ui-manager) → 0755. Module-private leaves (`/var/lib/secubox/<mod>` 0750) left
|
||||
untouched. Scaffold `new-package.sh` + `.claude/PATTERNS.md` fixed so new
|
||||
packages don't reintroduce it. secubox-core 1.1.8 tmpfiles.d now declares all 5
|
||||
shared parents at 0755 (mode-only) for boot/install-time self-heal.
|
||||
- **Verified:** all 64 changed maintainer scripts `bash -n` clean; zero bare-parent
|
||||
restrictive lines remain (install-d + chmod forms); saas-relay + core rebuilt and
|
||||
packaged postinst/tmpfiles confirmed. Two-stage review (found + closed 2 gaps:
|
||||
the chmod-form clobbers + tmpfiles coverage). NOT mass-deployed (60-pkg restart =
|
||||
thundering-herd risk); live covered by `secubox-dirs-guard.timer`; lands at next
|
||||
CI image build / reflash.
|
||||
|
||||
## 2026-06-18 — perf sprint (hub latency, R3 tunnel encoding) + crowdsec unblock
|
||||
|
||||
- **Hub dashboard latency (#644, PR #645, hub `1.4.6`).** The hub runs mounted in
|
||||
`secubox-aggregator` (no sub-app lifespan → cold caches); cold `/dashboard` fanned
|
||||
out ~16 sequential `systemctl is-active` (9-12 s) and `/public/health-batch` did an
|
||||
uncached 3.3 s `list-units`. Fix: `_ensure_services_warm()` (one batched offloaded
|
||||
`is-active`, double-checked lock vs thundering herd) on dashboard/status/modules/
|
||||
alerts; `_refresh_health_batch()` TTL snapshot served by the bg loop, cold-miss =
|
||||
one offloaded call. **Verified live: health-batch 3.3 s → 8 ms** (77 modules, shape
|
||||
unchanged). Toolbox `/admin/clients/rich` enrichment capped to the 12 most-recent.
|
||||
- **R3 tunnel web-load (#646, PR #647, toolbox `2.6.53`).** Diagnosed live: 4-core
|
||||
board at load ~5; the 4 mitm-wg workers are GIL-bound (~1 core total, ceiling ~30%/
|
||||
worker) competing with R2-mitm/gitea/metrics/crowdsec. Hot path already cached. The
|
||||
one code fix: `inject_banner` forced `Accept-Encoding: identity` on EVERY document
|
||||
for stream-inject, but streaming is disqualified on CSP-strict sites + when upstream
|
||||
compresses → those pages pulled uncompressed (3-5× bytes) through the worker for
|
||||
zero benefit. Now adaptive: keep gzip/br by default, learn per-host eligibility
|
||||
(`_STREAM_VERDICT`, capped/self-healing), strip identity only on proven-eligible
|
||||
hosts' next visit. No feature loss; workers came back leaner (72 MB vs 117 MB).
|
||||
Deploy via detached `dpkg -i` + rolling sequential restart of the 4 workers.
|
||||
- **crowdsec unblocked.** Its postinst's `cscli hub update` had 403'd
|
||||
(cdn-hub.crowdsec.net) leaving it half-configured (blocking apt). Re-tested → the
|
||||
403 was TRANSIENT CloudFront throttling (HTTP/2 200, real Amazon cert, not WAF-
|
||||
intercepted); `dpkg --configure crowdsec` → RC=0, `dpkg --audit` clean. No patch.
|
||||
|
||||
## 2026-06-15 — gitea mis-route fix + robust WAF route propagation
|
||||
|
||||
- **gitea (`git.maegia.tv`) 404 → 200.** Pure routing-table error: its WAF
|
||||
route pointed at `192.168.1.200:8000` (unrelated nginx) instead of the gitea
|
||||
LXC `10.100.0.40:3000`. Corrected the route; gitea container was healthy
|
||||
throughout. (`gitea.gk2`→nginx:9080 and `git.gk2`→gitea:3000 were already OK.)
|
||||
- **Robust route propagation (#609/PR #610, mitmproxy 1.0.8 + waf 1.2.6).**
|
||||
Fixing gitea surfaced that the #603 *file* bind-mount binds an inode, so route
|
||||
tools (`jq > tmp && mv` = new inode) didn't reach the addon until a container
|
||||
restart. Now: **directory** bind-mount (host `/srv/mitmproxy` →
|
||||
`/var/lib/secubox-waf-routes`, ro) + symlink, and the addon **live-reloads**
|
||||
`haproxy-routes.json` on mtime change (10 s throttle, in `requestheaders`).
|
||||
Verified live: `jq+mv` add → `[routes] live-reloaded 256 routes`, **0
|
||||
restart**. Ported to source (both synced `secubox_waf.py` copies + wafctl) +
|
||||
rebuilt into apt.secubox.in.
|
||||
|
||||
## 2026-06-15 — WAF hardening + perf: close open-proxy, behind-WAF media cache
|
||||
|
||||
Follow-up to the WAF restoration. Three findings investigated; two fixed.
|
||||
|
||||
- **Open forward-proxy / loops (#605/PR #606, mitmproxy 1.0.6 + waf 1.2.4).**
|
||||
`--mode regular` + HAProxy `default_backend mitmproxy_inspector` made the WAF
|
||||
an open proxy: internet scanners (114.66.25.146, 211.154.17.165,
|
||||
hashtagbrock.nl) drove a **72% backend-error rate** + 11 self-loop 508s/hr.
|
||||
The `requestheaders` hook now serves ONLY our vhosts (routes / our domains
|
||||
via routes-derived `local_suffixes` → nginx :9080 / `SELF_HOSTS`) and returns
|
||||
**421 with no upstream connect** otherwise. Live: 0 external server-connects,
|
||||
0 loop-508s, apt/admin/kbin 200, scanners 421.
|
||||
- **Behind-WAF media cache (#607/PR #608, mitmproxy 1.0.7 + waf 1.2.5).** New
|
||||
`media_cache.py` addon caches cacheable GET media/static (image/video/audio/
|
||||
font/css/js) from our vhosts on disk (URL key, 16 MB/obj, 2 GB LRU, TTL from
|
||||
`max-age`) and serves repeats from cache — backend-load + latency win for
|
||||
hosted media. **Not a bypass**: requests still pass `secubox_waf` inspection;
|
||||
only the response body is served from a WAF-populated cache. Toggle
|
||||
`/data/mitmproxy/media-cache.json` (default on). Live: `X-SecuBox-Cache: HIT`.
|
||||
Gate fix vs the toolbox copy: cache on body length (our nginx is chunked).
|
||||
- **WG R3 tunnel** (`wg-toolbox`, 4 peers, 4 `mitm-wg-worker@{1..4}`) is
|
||||
healthy — not the bottleneck; the WAF open-proxy churn was. All fixes ported
|
||||
to source (both synced `secubox_waf.py` copies) + rebuilt into apt.secubox.in.
|
||||
|
||||
**Still optional:** relax the forced `Connection: close` (FD-leak fix #496) to
|
||||
bounded keep-alive now that scanner churn is gone — lower per-request latency.
|
||||
|
||||
## 2026-06-15 — APT repo: all packages published + signed (apt.secubox.in)
|
||||
|
||||
Made the apt repo at `https://admin.gk2.secubox.in/repo/` (served from
|
||||
`/var/www/apt.secubox.in`, manager `repoctl`/reprepro) carry **all** packages.
|
||||
|
||||
- **Was broken**: pool had 15 orphan debs with an **empty reprepro DB** and no
|
||||
working signature — the published signing key `packages@secubox.in`
|
||||
(fp 31848880…) has **no private key on the board**.
|
||||
- **Signing** (user chose on-board `apt@secubox.in`, fp 219BA872…): imported its
|
||||
secret into the repo GPG home (`/var/lib/secubox-repo/gpg`), wrote
|
||||
`conf/distributions` (`SignWith: 219BA872…`) + `conf/options`, re-published
|
||||
`secubox-keyring.gpg` + `FINGERPRINT.txt`. `InRelease`/`Release.gpg` now
|
||||
**Good signature**. (install.sh doesn't pin the fp — transparent.)
|
||||
- **Built all 144 packages** (`-d`, arch:all) + `reprepro includedeb bookworm`
|
||||
→ 288 entries (×2 arch), 145 debs in pool, current versions
|
||||
(core 1.1.6, threat-analyst 1.4.4, vm 1.0.1, toolbox 2.6.37, hub 1.4.3).
|
||||
WebUI `/api/v1/repo/packages` lists 288. Served + signed via nginx :9080.
|
||||
- **Tooling fix**: `scripts/build-packages.sh` now passes `-d` to
|
||||
dpkg-buildpackage (it omitted it → dpkg-checkbuilddeps silently dropped
|
||||
secubox-core and others from every build). 1 pkg failed (sentinelle-gsm,
|
||||
buildinfo artifact race — deb still produced).
|
||||
|
||||
**Public HTTPS now works — WAF mitmproxy restored (3 stacked bugs).** The WAF
|
||||
LXC (`mitmproxy`, served via HAProxy `mitmproxy_inspector` → 10.100.0.60:8080)
|
||||
was down board-wide (every inspected vhost 503/400), blocking public
|
||||
`apt.secubox.in`. Three compounding faults, all fixed live on gk2:
|
||||
|
||||
1. **Crash-loop** (restart #45552): the `cookie-audit.conf` systemd drop-in
|
||||
(added #156) overrode `ExecStart` but dropped `--set confdir=/data/mitmproxy`
|
||||
→ mitmdump fell back to `~/.mitmproxy`, which `ProtectHome=true` blocks →
|
||||
`PermissionError: config.yaml`. Restored the flag in the drop-in (+ copied
|
||||
the existing CA into `/data/mitmproxy` to preserve identity).
|
||||
2. **mitmproxy-11 routing**: the LXC addon (`secubox_waf.py`, pre-#499) only
|
||||
redirected upstream in the `request` hook, but mitmproxy 11 opens the
|
||||
upstream connection *before* `request` → traffic went to the public IP
|
||||
(82.67.100.75). Added a `requestheaders` hook that sets
|
||||
`flow.server_conn.address` (+ request host/port) before the connect.
|
||||
3. **Route-file drift** (the real killer, `routes_count: 0`): the addon reads
|
||||
`/data/mitmproxy/haproxy-routes.json`, but the system maintains
|
||||
`/srv/mitmproxy/haproxy-routes.json` (255 routes). The addon's file was
|
||||
missing. Fixed by **bind-mounting** the host file into the container at the
|
||||
addon's path (`/var/lib/lxc/mitmproxy/config`) so they stay in sync.
|
||||
|
||||
Verified: `apt-get update` against `https://apt.secubox.in` fetches a
|
||||
**GPG-signed** InRelease + Packages (no signature errors), apt sees 130
|
||||
secubox packages, `.deb` downloads (200). Other inspected vhosts recovered.
|
||||
Live fixes are durable (container rootfs + LXC config survive restarts);
|
||||
porting them into the provisioning package is a follow-up.
|
||||
|
||||
## 2026-06-15 — threat-analyst: global security overview (1.4.3, live on gk2)
|
||||
|
||||
`secubox-threat-analyst` 1.4.1 → 1.4.3, merged via **PR #598 (closes #597)**,
|
||||
built + deployed live on gk2.
|
||||
|
||||
- **#597** — threat-analyst page becomes a **global security overview**: all
|
||||
metrics dynamic, fed live from WAF + CrowdSec + firewall. New cached
|
||||
`/overview` endpoint (double-buffer, 60 s background refresh →
|
||||
`overview.json`) aggregating WAF (`/run/secubox/waf.sock /stats`: threats
|
||||
today, blocked 24 h, rules loaded), CrowdSec (detection: alerts), firewall
|
||||
(enforcement: IPs blocked in nft via crowdsec-firewall-bouncer). WebUI gains
|
||||
a "Vue globale sécurité" card row + source health line (`loadOverview()` in
|
||||
`loadAll()`).
|
||||
- **Privilege-safe sourcing**: daemon runs as unprivileged `secubox` user →
|
||||
`cscli`/`nft list` (both root-only) failed silently. Switched to CrowdSec's
|
||||
privilege-free **Prometheus :6060** (`cs_alerts` + `cs_active_decisions`).
|
||||
No privilege escalation, no coupling to broken `secubox-blacklist-sync`.
|
||||
- Also carried the **1.4.2 build-safe postinst** fix (#595/#596) which had
|
||||
not yet reached the board (was at 1.4.1; `deb-systemd-helper` enable).
|
||||
- Live verified: CrowdSec 3712 alerts / 29312 active decisions, firewall
|
||||
29312 blocked, WAF 140 rules; `/overview` 200 via socket **and** aggregator
|
||||
proxy (aggregator restarted to re-discover the new route).
|
||||
|
||||
**Found, not fixed (separate):** `secubox-blacklist-sync.service` is **failed**
|
||||
(#521, exit 2) → `secubox_blacklist` nft sets empty. Does not affect the
|
||||
overview (firewall count comes from the bouncer via Prometheus).
|
||||
|
||||
### 1.4.4 — real CrowdSec ingestion (#599, PR #600)
|
||||
|
||||
The overview cards populated, but the **headline stats + Top-N leaderboards
|
||||
stayed 0**: `collect_crowdsec_alerts()` shelled out to bare `cscli`, which
|
||||
fails for the unprivileged `secubox` user → `alerts.jsonl` empty.
|
||||
|
||||
- **Read-only sudo ingestion** (backend only; frontend stays value-only):
|
||||
collector now runs `sudo -n /usr/bin/cscli alerts list -o json -l 200`.
|
||||
Ships `/etc/sudoers.d/secubox-threat-analyst` (only `cscli alerts/decisions
|
||||
list *`, read-only), `visudo`-validated in postinst (self-removes if bad).
|
||||
- **`NoNewPrivileges=no`** on the unit so sudo can escalate — matches the
|
||||
sibling `secubox-crowdsec` / `secubox-waf` units (`NoNewPrivileges=yes`
|
||||
had blocked sudo: "no new privileges flag is set").
|
||||
- **Auto-collect loop** (~5 min) fills the DB without the page open; severity
|
||||
mapped correctly (`remediation` is a bool).
|
||||
- **Dedup + 48 h compaction**: `get_recent_alerts` dedups by id, `compact_
|
||||
alerts()` bounds the append-only log (was inflating counts/leaderboards).
|
||||
- Live verified (1.4.4): `alerts_24h=12`, **13 unique IPs, 10 countries**
|
||||
(BG/BR/DE/FR/ID/IE/JP/NL/SG/US), 6+ scenarios → stats + leaderboards real.
|
||||
|
||||
### secubox-vm 1.0.1 — /vm/ showed 0 containers (#601, PR #602)
|
||||
|
||||
`https://admin.gk2.secubox.in/vm/` reported 0 containers though gk2 runs 20
|
||||
LXC (16 running). Two compounding bugs:
|
||||
|
||||
- **Privilege**: the **aggregator mounts each module in-process** as the
|
||||
unprivileged `secubox` user (serving model confirmed:
|
||||
`/usr/lib/python3/dist-packages/aggregator/main.py` imports
|
||||
`/usr/lib/secubox/<name>/api/main.py`). Bare `lxc-ls` can't see root's
|
||||
`/var/lib/lxc` → empty.
|
||||
- **Wrong `-F` key**: `lxc-ls -F MEMORY` is rejected (`Invalid key`) and emits
|
||||
no rows — valid key is `RAM`.
|
||||
|
||||
Fix (backend-only): LXC read+lifecycle via `sudo -n` (`run_priv`); ships
|
||||
`/etc/sudoers.d/secubox-vm` (`lxc-ls/info/start/stop`, visudo-validated);
|
||||
`lxc-create`/`destroy` stay root-only (endpoints carry no JWT); `lxc-ls -F
|
||||
…,RAM`; postinst reloads `secubox-aggregator`. KVM/libvirt readings were
|
||||
already correct (`/dev/kvm` absent, libvirtd off). Live: `containers
|
||||
{total: 20, running: 16}`, `/vms` lists all 20.
|
||||
|
||||
## 2026-06-14 — ToolBoX privacy/perf sprint : 2.6.23 → 2.6.36, all live on gk2
|
||||
|
||||
Large feature sprint on `secubox-toolbox` (built + merged + deployed live,
|
||||
kbin healthy) + clients + two live fixes. Each shipped via PR + merge +
|
||||
build + deploy.
|
||||
|
||||
**Toolbox (`secubox-toolbox` 2.6.23 → 2.6.36):**
|
||||
- #560 protective mode — tracker alerting + active **spoofer** (strip
|
||||
operator/tracking headers, drop 3rd-party cookies, DNT/GPC). Live in
|
||||
`spoof` on the 4 R3 workers + R2.
|
||||
- #566 modular **filters** (`/etc/secubox/toolbox/filters.json`, WebUI
|
||||
`/admin/filters/ui`) + R3+/R4 **ad/banner ghoster** (ad-hiding CSS +
|
||||
204 ad/tracker hosts ; savings → banner quick-stats).
|
||||
- #584 ad ghosting = **collapse** (no placeholder ; reverted #576 black-hole).
|
||||
- #577 shared **media proxy-cache** (image/video-segment, 16 MB/obj cap,
|
||||
2 GB LRU, default OFF/opt-in) — `/admin/cache`.
|
||||
- #589/#591 **autolearn** bad trackers → ad_ghost block set (threat-intel
|
||||
domains + operator-grade cross-site ; anti-bot excluded) + hourly timer.
|
||||
- #553/#549 cartographie **donut** (continent→country) + #587
|
||||
**domain-nugget** cloud (country→eTLD+1) + #575 **IP nodes hidden**
|
||||
(flag+name only) + #555 **favicons** of major sites (never IPs).
|
||||
- #545/#572 banner: neon → colourful **emoji-chip guirlande** ;
|
||||
inspected→**protected** on R3+/R4 ; #578 shared **pin** broadcast
|
||||
(`/admin/pin/ui`).
|
||||
- #570 DPI **media/content-type statistifier** + donut (`/admin/media/ui`).
|
||||
- #574 webext popup **protection panel** ; #568 top-tracker list capped 5.
|
||||
- #562 `/ca/fingerprint` surfaces the **R3 CA** (D5:E4:3A) on the tunnel.
|
||||
- #581 **postinst fix** : enabled units get a real `restart` on upgrade
|
||||
(was leaving the portal dead → kbin 503 ; bit us twice).
|
||||
- #516 review (#564): `detect_antibot` → (vendor, **is_challenge**),
|
||||
response-level (cf-mitigated / non-200 token) — deployment vs challenge.
|
||||
|
||||
**Clients:** Android APK **v0.3.0** (real zero-tap : launch + boot
|
||||
auto-onboard) ; webext **v0.1.4** (crash-fix const-ext, favicons, popup
|
||||
protection panel) — both served from the cabine + GitHub releases.
|
||||
|
||||
**Live fixes:** Nextcloud iPhone photo sync (disabled broken
|
||||
`files_antivirus` + raised PHP upload limits) ; kbin 503 root-caused →
|
||||
#581.
|
||||
|
||||
**Open / blocked:** #592 unified webmail-hub (Gmail OAuth2 + Gandi + OVH) —
|
||||
design filed, BLOCKED on a Google OAuth client + operator decisions.
|
||||
|
||||
## 2026-06-13 — Browser extension : emancipate cartographie live (ref #532)
|
||||
|
||||
Nouveau client `clients/webext-toolbox/` (MV3 Firefox `.xpi` + Chromium),
|
||||
|
|
@ -6758,19 +6091,3 @@ CONFIG_USB_NET_RNDIS_HOST=y
|
|||
- LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1
|
||||
- ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online
|
||||
- STALE, FAILED = offline
|
||||
|
||||
## 2026-06-24 — build+deploy T0 fixes (#494/#519/#53/#421) + dirs-guard /run self-heal
|
||||
|
||||
- Merged #121/#53/#65; cherry-picked #494 onto master (versions re-bumped above
|
||||
master's advanced core 1.1.8/hub 1.4.6 → core 1.1.9, hub 1.4.7).
|
||||
- Discovered #494 was systemic (7 pkgs chowning /run/secubox parent) AND that
|
||||
91 services declare `RuntimeDirectory=secubox` → systemd re-chowns the parent
|
||||
to secubox:secubox 0755 on each start (#421). Central fix: extended
|
||||
secubox-dirs-guard to re-assert /run/secubox 1777 root:root every minute
|
||||
(core 1.1.10) instead of editing 91 units.
|
||||
- Built + deployed to gk2 (8 pkgs): core 1.1.10, hub 1.4.7, eye-remote 1.0.1,
|
||||
metablogizer 1.2.2, metrics 1.0.4, p2p 1.7.1, wazuh 1.0.1, toolbox 2.7.18.
|
||||
First deploy ssh was timeout-killed mid-toolbox-postinst → recovered with
|
||||
dpkg --configure -a (cleared stale lock). Verified: /run/secubox=1777 root:root
|
||||
holds, 0 half-configured, all services + R3 workers active, webui/portal 200,
|
||||
toolbox blacklist-sync (#519) carried.
|
||||
|
|
|
|||
|
|
@ -383,13 +383,9 @@ case "$1" in
|
|||
adduser --system --group --no-create-home --home /var/lib/secubox secubox
|
||||
fi
|
||||
|
||||
# Répertoires runtime — SHARED parents, NE JAMAIS les passer en 0750/0700
|
||||
# (#623 : casse la traversée pour les daemons non-secubox → kbin/toolbox 500).
|
||||
# /run/secubox reste 1777 (sticky world-writable, sockets de tous les services,
|
||||
# #471) ; /var/lib/secubox reste 0755. Les leaves privées
|
||||
# (/var/lib/secubox/<module>) peuvent être 0750.
|
||||
install -d -o root -g root -m 1777 /run/secubox
|
||||
install -d -o secubox -g secubox -m 755 /var/lib/secubox
|
||||
# Répertoires runtime
|
||||
install -d -o secubox -g secubox -m 750 /run/secubox
|
||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox
|
||||
|
||||
# Activer et démarrer le service
|
||||
systemctl daemon-reload
|
||||
|
|
|
|||
180
.claude/TODO.md
|
|
@ -1,185 +1,10 @@
|
|||
# TODO — SecuBox-DEB Backlog
|
||||
*Mis à jour : 2026-06-27*
|
||||
|
||||
---
|
||||
|
||||
## ⚪ T5 — Images / OS variants / Hardware (ajouts 2026-06-27)
|
||||
|
||||
### ⬜ MOCHAbin — bootloader propre (adresses réservées + extlinux)
|
||||
|
||||
> Workaround actif : `/boot/boot.scr` compilé forçant le kernel à `0x0a000000`. Fix durable requis.
|
||||
|
||||
- [ ] **Option A — Corriger l'image** : patcher `extlinux.conf` généré par le CI pour utiliser
|
||||
`0x0a000000` (kernel) et `0x10000000` (initrd) au lieu de `0x02080000` (adresse réservée
|
||||
factory U-Boot 2020.10 → reset immédiat). Boot.scr deviendrait redondant.
|
||||
- [ ] **Option B — Enhanced Tow-Boot (#748)** : bloqué par le ciseau U-Boot (voir ci-dessous) ;
|
||||
déverrouille wget/HTTP natif dans U-Boot, supprime le besoin de TFTP pour les futures installs.
|
||||
- [ ] **Valider** que le fix d'adresse tient sur les deux MOCHAbin (gk2 + c3box).
|
||||
|
||||
### ⬜ #748 — wget dans U-Boot pour MOCHAbin (bloquant documenté)
|
||||
|
||||
> Bloquant dur (ciseau) confirmé 2026-06-27. Branche
|
||||
> `feature/748-enhanced-tow-boot-http-netboot-serial-fl` : spec + plan + Kconfig +
|
||||
> `build-uboot-overlay.sh --tow-boot` + CI `.github/workflows/build-tow-boot.yml` en place.
|
||||
> Problème : board mochabin UNIQUEMENT dans fork Tow-Boot U-Boot 2022.07 (pas de `wget`) ;
|
||||
> `wget`/TCP UNIQUEMENT dans stock U-Boot ≥2023.07 (pas de board mochabin/DTS).
|
||||
|
||||
- [ ] **Voie 1** : backporter le stack TCP + `wget` de U-Boot ≥2023.07 dans le fork Tow-Boot
|
||||
2022.07 (mochabin board natif). Diff TCP = `net/wget.c` + dépendances `CONFIG_NET_WGET`.
|
||||
- [ ] **Voie 2** : porter le board mochabin (DTS Armada 7040 + PHY + eMMC) vers U-Boot mainline
|
||||
≥2023.07 (sans Tow-Boot). Plus long mais durable.
|
||||
- [ ] Choisir une voie, débloquer #748.
|
||||
|
||||
### ⬜ Packager le flow netboot + install signé (rig temporaire → procédure reproductible)
|
||||
|
||||
> Actuellement rig manuel sur gk2 : `lan1=192.168.77.1/24`, dnsmasq DHCP, nft, nginx `:8099`.
|
||||
|
||||
- [ ] Scripter la publication de l'image signée dans le root HTTP netboot (wget + sha256 + sig).
|
||||
- [ ] Documenter / packager la config dnsmasq + nft + nginx pour un segment `lan1` dédié.
|
||||
- [ ] Intégrer dans `scripts/deploy-netboot.sh` ou équivalent.
|
||||
|
||||
### ⬜ Teardown rig netboot temporaire gk2
|
||||
|
||||
> Le rig (lan1 bridge, dnsmasq, nft iif lan1 accept, nginx extra listen) reste actif jusqu'à
|
||||
> ce que c3box soit autonome en prod.
|
||||
|
||||
- [ ] Retirer la règle nft `iif lan1 accept` (risque : tout le segment lan1 est accepté sans filtrage).
|
||||
- [ ] Désactiver / retirer dnsmasq test sur lan1.
|
||||
- [ ] Retirer le extra listen `192.168.77.1:8099` du vhost nginx netboot (ou couper le vhost si
|
||||
plus nécessaire).
|
||||
- [ ] Vérifier que c3box auto-boot sans rig (boot.scr en place → OK).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Clos 2026-06-22 — DPI exfil + report Netrunner + sbxmitm
|
||||
|
||||
- ✅ **#687 DPI exfil pipeline** — flowcap + Go collector + dashboard + cumulatif 7j,
|
||||
packagé `secubox-dpi 1.1.2` (inclut #692/#693/#695/#705).
|
||||
- ✅ **#707 report kbin = fiche Netrunner** HTML+PDF (#699/#701/#703/#709/#711/#714/#716).
|
||||
- ✅ **#689** sbxmitm cert 365d · **#697** stream >8MiB (Gmail) · **#688** splice rejeté.
|
||||
|
||||
### DPI Phase 3
|
||||
- [x] Enrichissement **ASN** (GeoLite2-ASN) pour l'egress sans SNI — **#719 mergé, live**
|
||||
(`secubox-dpi 1.1.3`, maxminddb-golang vendored).
|
||||
- [x] **Historique + timeline par device** — **#721 mergé, live** (`secubox-dpi 1.1.4`,
|
||||
buckets quotidiens `history.json` 14j + `/api/v1/dpi/history` + panneau Timeline
|
||||
dashboard). NB : JSON daily buckets (pas SQLite — pas de driver CGO dans le binaire
|
||||
statique ; SQL riche reportable si besoin).
|
||||
- [x] Démon **nDPId** — **évalué puis ÉCARTÉ** (#722/#723 revertés). Raison perf :
|
||||
ndpiReader tourne en fenêtres bornées (Nice 15, ~1% CPU, libère le cœur entre
|
||||
les passes) ; nDPId = démon permanent + nDPIsrvd → CPU/RAM **continue** sur une
|
||||
board déjà saturée (load ~4.6/4 cœurs). Gain (JSON riche, pas de respawn) <
|
||||
risque. **Décision : on garde ndpiReader** comme producteur du pipeline exfil.
|
||||
(Le build CI QEMU a aussi échoué au 1er essai → chemin fragile en plus.)
|
||||
|
||||
### ⬜ Cosmétique report PDF (non bloquant)
|
||||
- [ ] Glyphes drapeaux régionaux → lettres (police embarquée). Option : drapeaux PNG.
|
||||
- [ ] Chiffres espacés dans certaines cellules (fallback police).
|
||||
|
||||
### ⬜ APK on-device #685/#686 — NON-ROOT ONLY (plan verrouillé, à faire)
|
||||
> Décision 2026-06-22 : cible **non-root uniquement** ; chemin root abandonné.
|
||||
> Plan détaillé : commentaire #685.
|
||||
- [ ] **VpnService in-app** (`com.wireguard.android:tunnel` / GoBackend wireguard-go)
|
||||
— l'APK EST le client WG, plus de Play Store, détection tunnel in-app fiable.
|
||||
- [ ] **CA en DER** (fix « nom de cert vide » du KeyChain intent) + `network-security-config`
|
||||
pour que la WebView in-app fasse confiance au CA ca-wg.
|
||||
- [ ] Retirer RootShell/RootOnboard/BootReceiver ; manifest VpnService + consent VPN.
|
||||
- [ ] Limite Android : pas de CA **système** sans root → MITM système impossible ;
|
||||
surface « safe browsing » = WebView in-app. À documenter.
|
||||
- [ ] Build via CI `build-android-apk` + **test sur appareil** (gros build, itératif).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Backlog priorisé — revue 2026-06-24 (64 issues ouvertes)
|
||||
|
||||
> Index d'autorité du triage. Les sections « Phase X » plus bas sont historiques :
|
||||
> plusieurs portent « ✅ COMPLETE » alors que l'issue est restée **ouverte** (livré
|
||||
> mais jamais fermé) → marquées **[vérifier→fermer]** ci-dessous.
|
||||
|
||||
### 🔴 T0 — Régressions & bugs sécurité (petits, débloquants, CSPN priv-sep)
|
||||
- #494 secubox-core ExecStart écrase tmpfiles.d `/run/secubox` *(worktree actif)*
|
||||
- #468 `/etc/secubox` parent 0750 casse la traversée non-secubox *(régression récurrente)*
|
||||
- #471 secubox-mesh postinst écrase perms `/run/secubox` *(régression)*
|
||||
- #421 sockets `/run/secubox` cachés en mount-ns privé (RuntimeDirectory)
|
||||
- #447 kiosk : mot de passe admin semé par le CI (users.json shippe un hash) **← fuite**
|
||||
- #91 haproxyctl régénère haproxy.cfg avec `waf_inspector` inexistant *(intégrité WAF)*
|
||||
- #65 nginx : routes API manquantes dans webui.conf
|
||||
- #53 Wazuh uvicorn 100% CPU spin
|
||||
- #121 metablog ingest : dirs en `secubox:secubox`
|
||||
|
||||
### 🟠 T1 — Plan d'enforcement sécurité (mission CSPN ; détection→action)
|
||||
- #498 Phase 7 — WAF active enforcement (mitm→CrowdSec→nft drop) *(worktree actif)*
|
||||
- ✅ #519 Phase 13 — enforcement plane **FERMÉ 2026-06-22** (livré + réparé :
|
||||
blacklist-sync avortait sur NXDOMAIN + timeout unit → fix `|| true` +
|
||||
TimeoutStartSec 600 ; vérifié live, default-off). Inclut 13.B #522.
|
||||
- #455 secubox-egress — détection egress + corrélation RDS multi-signaux
|
||||
- #500 Phase 8 — Utiq operator-grade tracking (detect/alert/bypass)
|
||||
- #514 Phase 12 — plateforme anti-human-detection (parent ; sous-tracks fermés)
|
||||
- ✅ #515 Phase 12.A CDN cache detection — **FERMÉ** (live, `social_host_meta.cdn_vendor`)
|
||||
- ✅ #516 Phase 12.B anti-bot detection — **FERMÉ** (live via #564/#565, `social_antibot`)
|
||||
- #525 Phase 14 — plan de déception (idée future, parké)
|
||||
- ⬜ Suivi #519 perf (non bloquant) : DNS-guard ne résout que les 2000 premiers
|
||||
domaines/cycle (5523 en base) → couverture partielle ; résolution séquentielle
|
||||
lourde sur board saturé. Option : résolution parallèle bornée + rotation du cap.
|
||||
|
||||
### 🟡 T2 — UX / Hub / conscommateurs report (worktrees actifs + polish)
|
||||
- #615 security-posture dans la sidebar Hub *(worktree actif)*
|
||||
- #655 webext content-script banner CSP-immune *(worktree actif)*
|
||||
- #485 toolbox SOC scoring *(worktree actif)*
|
||||
- #513 ToolBox WebUI : sous-onglets + retrait UI /admin redondante
|
||||
- #69 diagramme flux trafic responsive
|
||||
- #67 cache history-aware glances/netdata
|
||||
- #68 health checks + dépendances services au démarrage
|
||||
|
||||
### 🟢 T3 — Backlog feature (valeur, non bloquant)
|
||||
- #685 APK 'corrupt' — CI signe avec clé éphémère *(plan APK verrouillé)*
|
||||
- #686 android-toolbox flux non-root cassé *(plan APK verrouillé)*
|
||||
- #429 nextcloud dashboard : API stubs au lieu de la vraie instance *(bug)*
|
||||
- #430 nextcloud — fédération OCM (doc/outillage)
|
||||
- #472 nextcloud — Gondwana Desktop (canvas + widgets)
|
||||
- #592 secubox-webmail-hub (Gmail OAuth2 + Gandi + OVH)
|
||||
- #66 auth Google OAuth
|
||||
- #70 Health Banner System *(preplanned)*
|
||||
- #71 CDN proxy injection *(preplanned)*
|
||||
- #393 source-home des scripts health prober
|
||||
|
||||
### 🔵 T4 — Hardware-gated (dépend de pièces ; piste parallèle ; pas de spare EP06)
|
||||
- Modem/PCIe : #254 modules kernel LTE · #255 pins mPCIe modem · #460 DTS cp0_pcie2 ·
|
||||
#467 U-Boot comphy5 SerDes · #462 pivot HW AR9271/MT
|
||||
- Mesh/BLE : #449 WiFi 802.11s · #452 BT mesh · #453 QR multi-canaux · #454 sourcing BLE 5.x
|
||||
- GSM : #347 sentinelle-gsm
|
||||
- Smart-Strip : #33 module HMI · #42 sous-repo · #379 packaging
|
||||
- Eye-remote : #41 sous-repo · #79 buildroot · #127 variante square · #138 radar_concentric ·
|
||||
#155 collision link-rename *(bug)* · #158 multi-gadget L3 · #478 métriques live Round Eye
|
||||
- VILLAGE3B : #480 dossier presse · #497 poster grand public
|
||||
|
||||
### ⚪ T5 — Images / OS variants (basse urgence)
|
||||
- #446 Full Traveller OS multi-mode/arch · #125 build-live-usb +virtualbox · #422 vm-x64 cascade
|
||||
|
||||
### ⚫ T6 — Docs / housekeeping
|
||||
- #81 headers SPDX CMSD-1.0 partout · #243 clarifier scope secubox-zkp-auth *(question)*
|
||||
- #474 ToolBoX (epic parent — garder comme tracker)
|
||||
*Mis à jour : 2026-06-13*
|
||||
|
||||
---
|
||||
|
||||
## 🔥 P0 — Immediate (in flight)
|
||||
|
||||
### kbin Tor endpoint — anonymized quick-switch surfing (#683)
|
||||
|
||||
> Capstone du couteau suisse cyber : l'anonymat de la sortie. Spec :
|
||||
> `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`.
|
||||
> Invariants : inspection préservée, fail-closed, opt-in (défaut OFF), no DNS leak, CSPN audit.
|
||||
|
||||
- [ ] **Transport** — Option A dialer SOCKS5 upstream (cœur Go #662, *préféré*) vs
|
||||
Option B nft mark → Tor TransPort (fallback pré-#662).
|
||||
- [ ] **Profil Tor egress** — réutiliser `secubox-exposure` (bootstrap/NEWNYM), egress-only.
|
||||
- [ ] **API toolbox** — `POST /admin/tor/{on,off}` (WG-hash scoped) + `GET /tor/state` +
|
||||
`POST /tor/newnym` + état SQLite per-client (TTL 24h).
|
||||
- [ ] **UI kbin** — toggle 🧅 + badge état + flag pays de sortie + bouton « nouvelle identité ».
|
||||
- [ ] **Leak-guard nft** + DNS-over-Tor (test exit IP + resolver ≠ Unbound).
|
||||
- [ ] **`tls_splice` OFF en mode Tor** (#649) — sinon les flux asset fuient l'IP réelle.
|
||||
- [ ] **CSPN** — audit-log chaque bascule ; soak DARK (flag présent, UI cachée) avant flip.
|
||||
|
||||
### ToolBox clients (`clients/`)
|
||||
|
||||
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,
|
||||
|
|
@ -191,9 +16,6 @@
|
|||
- [x] **#532 browser extension** (`clients/webext-toolbox/`) — MV3 Firefox
|
||||
`.xpi`/Chromium; live tracker badge + popup mini Round-Eye graph over
|
||||
`/social/*`; `GET /wg/toolbox.xpi` + fetch helper + `build-webext.yml`.
|
||||
- [x] **#532 release** — tag `webext-v0.1.1` published the `.xpi`
|
||||
(downloadable, verified 200). `make_latest:false` + tag-pinned URL so it
|
||||
doesn't steal "Latest" from the Android APK release.
|
||||
- [ ] **release signing** — Android keystore + AMO `.xpi` signing secrets in CI
|
||||
for stable published fingerprints (currently unsigned sideload).
|
||||
- [ ] **#532 follow-ups** — optional `GET /social/live/{token}` SSE (replace the
|
||||
|
|
|
|||
263
.claude/WIP.md
|
|
@ -1,253 +1,5 @@
|
|||
# WIP — Work In Progress
|
||||
*Mis à jour : 2026-06-27*
|
||||
|
||||
---
|
||||
|
||||
## ✅ 2026-06-27 : c3box → SecuBox Debian — première install réussie · netboot prouvé (#748 #737)
|
||||
|
||||
### ✅ Fait (session 2026-06-27)
|
||||
|
||||
- **Netboot gk2→c3box prouvé** — factory U-Boot 2020.10 → TFTP → rescue shell installeur
|
||||
(kernel 6.12.85 #5secubox). Détour cabling résolu (impasse LAB, pas logiciel).
|
||||
- **Première install SecuBox Debian sur un MOCHAbin physique (c3box)** — image CI artefact
|
||||
`secubox-mochabin-bookworm` (run 27426515472, 8 Gio), SHA256 + signature vérifiés,
|
||||
`gunzip|dd` en RAM → eMMC. c3box boot Debian v1.9.0 avec stack complète.
|
||||
- **boot.scr workaround déployé** — extlinux.conf charge le kernel à `0x02080000` (réservé
|
||||
factory U-Boot → reset). Construit `/boot/boot.scr` (kernel@`0x0a000000`) ; auto-boot
|
||||
Debian sans intervention vérifié après reboot.
|
||||
- **#748 bloquant documenté** — ciseau U-Boot : mochabin board UNIQUEMENT dans fork Tow-Boot
|
||||
2022.07 (pas de `wget`) ↔ `wget` UNIQUEMENT dans stock ≥2023.07 (pas de board mochabin).
|
||||
Branche `feature/748-enhanced-tow-boot-http-netboot-serial-fl` parkée (spec+CI+Kconfig en
|
||||
place, dépend du backport wget OU port board mainline).
|
||||
|
||||
### ⬜ Rig netboot temporaire gk2 à démonter (quand c3box autonome)
|
||||
|
||||
- `lan1=192.168.77.1/24` avec dnsmasq DHCP + `nft iif lan1 accept` + nginx `:8099` encore actifs.
|
||||
- À retirer une fois c3box en prod (voir TODO T5 — teardown rig).
|
||||
|
||||
### ⬜ Bootloader propre à faire (#748 ou alternative)
|
||||
|
||||
- boot.scr = workaround ; fix durable = enhanced Tow-Boot (#748, bloqué ciseau) OU corriger
|
||||
les adresses de boot dans l'image (extlinux.conf → `0x0a000000`). Voir TODO T5.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 2026-06-22 : triage issues (30 ouvertes → revue obsolètes)
|
||||
|
||||
- **Fermées (user-validé 2026-06-22)** : #722 (nDPId — décidé contre, reverté) ·
|
||||
#475 ToolBoX Phase 1 (live 2.7.x) · #502/#507/#508 Social mapping (carto +
|
||||
/social/me + report PDF live) · #495 Phase 5 mitm-LXC (superseded par #662 Go
|
||||
sbxmitm host) · #531 APK one-tap (superseded par #685/#686 non-root) ·
|
||||
#486 geoip/ASN+flags+catégories dans rapports (livré master : geo.py + dpi_class.py +
|
||||
report wiring ; complémentaire de #718 ASN collector ; worktree stale nettoyé) ·
|
||||
#515 CDN detection (live `social_host_meta.cdn_vendor`) · #516 anti-bot detection
|
||||
(live via #564/#565) · #519 enforcement plane (livré + **réparé** : blacklist-sync
|
||||
avortait NXDOMAIN + timeout unit → fix `|| true` + TimeoutStartSec 600, vérifié live,
|
||||
default-off ; inclut #522). Toolbox source bumpé 2.7.18 (fix live-patché sur gk2) ·
|
||||
#468 /etc/secubox traversal (source+live = 0755, secrets/CA enfants restent 0750).
|
||||
- **Actives (worktrees en cours)** : #655 webext banner · #615 security-posture ·
|
||||
#494 secubox-core ExecStart · #498 Phase 7 WAF enforcement · #485 SOC scoring.
|
||||
|
||||
### 🔎 Reco T0 — recon live gk2 2026-06-24 (avant fix)
|
||||
- ✅ **#494** : **FIX SYSTÉMIQUE poussé** (`fix/494-…`). Pas que core : 7 units re-chownaient
|
||||
le parent partagé `/run/secubox` (core+hub services, eye-remote/eye-square/metablogizer/
|
||||
metrics/p2p postinsts ; eye-square chownait aussi /var/log/secubox = pire). Tous nettoyés
|
||||
(mkdir fallback only ; logs modules en sous-dossier propre ; orphan /etc/tmpfiles.d nettoyé).
|
||||
**Vérifié live** : /run/secubox 1777 **root:root** stable après restart core ET hub ; webui 200.
|
||||
Bumps core 1.1.7/hub 1.4.4/eye-remote 1.0.1/eye-square 1.0.4/metablog 1.2.2/metrics 1.0.4/p2p 1.7.1.
|
||||
- ✅ **#471** (mesh /run/secubox) : déjà résolu (changelog mesh "drop install -d /run/secubox") → verify-close.
|
||||
- ⬜ **#421** : sockets cachés en mount-ns privé (RuntimeDirectory) — mécanisme distinct, non traité.
|
||||
- 🆕 Suivi (classe #511) : mesh/toolbox/admin font `install -d -o <module> /var/log/secubox`
|
||||
(propriétaire du parent partagé = user module) → autres daemons ne peuvent créer leurs logs.
|
||||
Séparé de #494, à traiter (sous-dossiers propres comme fait pour eye-square/p2p).
|
||||
- **#447** : pas une fuite — `password_hash=null` → lockout kiosk + user CI parasite ;
|
||||
**CI-image-gated** (rpi400, pas gk2).
|
||||
- **#91** : `haproxy.cfg` active valide ; backup `*.broken-by-haproxyctl-*` prouve le bug
|
||||
passé ; drift-guard #627 rattrape. Root cause = generate `haproxyctl` (api/main.py l.846/896).
|
||||
- ✅ **#53** : **FIX poussé** (`fix/53-…`) — gate `ConditionPathExists=/var/ossec/etc/ossec.conf`
|
||||
+ `RestartSec=5` ; module conservé (SIEM opt-in). Vérifié gk2 (/var/ossec absent). Bump 1.0.1.
|
||||
- ✅ **#65** : déjà résolu en prod (webui.conf déployé inclut `secubox-routes.d/*.conf`,
|
||||
163 snippets). Template `common/nginx/webui.conf` (stale) synchronisé sur `feature/65-…`.
|
||||
Reco fermer. Convention : `secubox-routes.d/`=actif, `secubox.d/`=legacy.
|
||||
- ✅ **#121** : **FIX poussé** (`fix/121-…`) — helper `fix_perms` chown -R secubox:secubox
|
||||
le site dir après chaque ingest .git (metablog-ingest-site.sh). Script dev, pas de deploy.
|
||||
- ⬜ Restent : **#91** (deploy WAF risqué) · **#65** (refactor include, risque 502) ·
|
||||
**#447** (CI kiosk) · **#494/#471/#421** (worktree fix/494). Build+deploy toolbox 2.7.18 (#519) en attente.
|
||||
- **Backlog/future** : #685/#686 APK non-root (plan verrouillé) · #592 webmail-hub ·
|
||||
#514/#515/#516/#519/#522/#525 Phase 12-14 (#515 CDN / #516 anti-bot partiellement
|
||||
couverts par antibot_sites/opgrade_sites du social graph) · #500 Utiq · #497/#480/
|
||||
#478 VILLAGE3B Eye/poster · #472/#430/#429 Nextcloud · #471/#468/#421 perms (à
|
||||
vérifier si déjà corrigées) · #467/#462/#460/#255/#254 hardware/kernel · #455 egress ·
|
||||
#454/#453/#452/#449 mesh/BLE · #448/#447/#446/#434 kiosk · #422 vm cascade ·
|
||||
#393/#379/#347 packaging · #513 WebUI sub-tabs.
|
||||
- ⚠️ Fermeture finale = **user only** (sauf issues créées en session) ; les
|
||||
recommandations ci-dessus sont commentées sur chaque issue.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 2026-06-22 : DPI exfil + Netrunner report + sbxmitm fixes (tous mergés, live gk2)
|
||||
|
||||
Session livrée intégralement sur master + déployée. Détail dans HISTORY 2026-06-22.
|
||||
|
||||
### ✅ Fait (mergé + live)
|
||||
- **DPI exfil pipeline (#687)** — `secubox-dpi 1.1.2` : flowcap (ndpiReader) → Go
|
||||
collector (catégories cloud/media/game/adult/ai/messaging/filehost/social + scénarios
|
||||
exfil) → `/api/v1/dpi/exfil` ; dashboard "Cloud Exfiltration Watch" + cartes repointées ;
|
||||
beaconing tuné (#692) ; cumulatif 7j `cumulative.json` (#705) ; packagé arm64.
|
||||
- **Report kbin = fiche Netrunner (#707)** — HTML (onglets Pistage/DPI/Overall + persona
|
||||
néon) **et** PDF (`_persona_block` + "En un coup d'œil" + grille donuts + carto + tables
|
||||
emoji). Charts en **PNG matplotlib** (#714, rendu universel iOS/Chrome) ; grille = une
|
||||
image 2×2 (#716, fin des 24 pages). Classe via UA live + niveau R3 auto (wg peer).
|
||||
- **sbxmitm** — cert forgé 24h→365d (#689, fin des "certificat expiré") ; fin de la
|
||||
troncature >8MiB (#697, Gmail OK) ; splice own-domain **rejeté** (#688, on intercepte tout).
|
||||
|
||||
### ⬜ Next Up (différé)
|
||||
- **#685/#686 APK on-device — NON-ROOT ONLY (plan verrouillé)** : VpnService in-app
|
||||
(wireguard-go), CA en DER + network-security-config WebView, retrait du chemin root.
|
||||
Gros build Android (CI + test device) → session dédiée. Détail : commentaire #685 + TODO.
|
||||
- **DPI Phase 3** — ✅ enrichissement ASN (#719, 1.1.3) · ✅ historique + timeline
|
||||
(#721, 1.1.4) · ❌ démon nDPId **écarté** (#722/#723 revertés) : risque perf
|
||||
(démon permanent vs fenêtres ndpiReader bornées) sur board saturée → **on garde
|
||||
ndpiReader**. **Phase 3 close.**
|
||||
- **#685 APK on-device** — install auto CA + handoff WG + détection tunnel (en attente
|
||||
décision rooted vs non-root du user).
|
||||
- **Cosmétique PDF** — glyphes drapeaux régionaux dégradent en lettres (police embarquée) ;
|
||||
chiffres légèrement espacés dans certaines cellules. Non bloquant.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 2026-06-19 : kbin Tor egress (#683) — ToolBoX 2.7.1, implémenté DARK
|
||||
|
||||
Switch + tunnel Tor quick-switch livrés sur `feature/683`, **défaut OFF / fail-closed**.
|
||||
Détail dans la section "Implémenté DARK" ci-dessous + HISTORY 2026-06-19.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 2026-06-19 : kbin milestone — ToolBoX 2.7.0 + chapitre Tor (plan)
|
||||
|
||||
Checkpoint de fin de session. Pas de changement de comportement runtime — docs +
|
||||
positionnement + version + plan de la lame suivante.
|
||||
|
||||
- ✅ **ToolBoX 2.7.0** (middle release) — clôt la ligne 2.6.x (ad-intelligence /
|
||||
Anti-Track v2 / anti-bot uTLS #662), ouvre le chapitre kbin « premier outil du
|
||||
couteau suisse cyber ». kbin = perf transparente + full encrypted + poison/smog +
|
||||
bandeau anti-adware + safe browsing.
|
||||
- ✅ **Docs kbin** — wiki [`Kbin-Toolbox.md`](../docs/wiki/Kbin-Toolbox.md),
|
||||
[`FAQ-KBIN-TOR.md`](../docs/FAQ-KBIN-TOR.md), blurb README.
|
||||
- ✅ **Plan #683** — spec
|
||||
[`2026-06-19-kbin-tor-anonymized-surfing-design.md`](../docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md) :
|
||||
endpoint Tor quick-switch (egress sortant, fail-closed, opt-in, no DNS leak,
|
||||
inspection préservée). Dépend du cœur Go #662.
|
||||
|
||||
### ✅ Implémenté DARK — chapitre Tor (#683, ToolBoX 2.7.1, branche feature/683)
|
||||
|
||||
- ✅ **Transport tranché** : *torify l'egress MITM* (owner-match nft sur l'uid
|
||||
`secubox-toolbox`/mitm-wg → Tor TransPort 9040 / DNSPort 5353). Inspection
|
||||
préservée. Décision USER (vs dialer SOCKS5 #662 = bloqué, vs torify client = casse
|
||||
l'inspection).
|
||||
- ✅ **Switch** : flags `tor_mode`/`tor_preset` (filters.json) ; API kbin-gated
|
||||
`GET/POST /admin/tor/{state,on,off,newnym,check-leaks}` ; onglet 🧅 WebUI (badge,
|
||||
toggle, NEWNYM, sonde fuite). `tor_ctl.py` réutilise le control-port de secubox-tor.
|
||||
- ✅ **Tunnel** : `conf/nft-toolbox-tor.nft` (fail-closed kill-switch + drop v6) +
|
||||
`conf/torrc-toolbox-egress.conf` + reconciler root path-triggered
|
||||
(`secubox-toolbox-tor.path` surveille filters.json → portail reste
|
||||
NoNewPrivileges=true). nft chargé AVANT tor (pas de fenêtre clearnet).
|
||||
- ✅ 166 tests verts ; license headers OK ; changelog 2.7.1.
|
||||
|
||||
#### ⬜ Avant flip ON (USER)
|
||||
|
||||
- Soak DARK puis `tor_mode=true` via l'onglet (admin.gk2).
|
||||
- Test de fuite **hors-board** : l'IP réelle de la box ne doit jamais apparaître.
|
||||
- Forcer `tls_splice` (#649) OFF quand armé (sinon flux asset fuient l'IP réelle).
|
||||
- **Per-client (WG-hash)** : nécessite le dialer SOCKS5 du cœur Go #662 (l'owner-match
|
||||
est global). Suivi sous #662.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 2026-06-17/18 : Anti-Track v2 + perf/ops sprint (gk2 live)
|
||||
|
||||
Tout mergé sur master + déployé sur gk2. Détail dans HISTORY 2026-06-18.
|
||||
|
||||
- ✅ **Anti-Track v2 (#633, PR #637)** — bloque/empoisonne/anonymise, moteur
|
||||
`privacy.py` + addon `privacy_guard.py`, learning (`learn.py`), IP-drop +
|
||||
unbound DNS-refuse (`ip_dns.py`/`escalate.py`), bypass-seed + #filtres badges,
|
||||
#social top-5. **Tourne DARK** (`privacy_enforce` unset). Wiki `Anti-Track.md`.
|
||||
- ✅ **Banner saga (#636/#639, PR #638/#640)** — mitm sert loader/bundle pour
|
||||
toute origine (PeerTube fixé), CSP fallback, top-bar, 1 bannière/visite.
|
||||
- ✅ **#634/#635** — reset-all clients + emojis device/flag/hosting.
|
||||
- ✅ **#642 (PR #643)** — social-graph ignore les edges IP-littéraux ; KPI
|
||||
"Trackers vus" = table.
|
||||
- ✅ **#644 (PR #645)** — hub dashboard/health-batch servis depuis cache TTL
|
||||
(health-batch 3.3 s → 8 ms) ; clients/rich enrichit 12 max. **hub 1.4.6**.
|
||||
- ✅ **#646 (PR #647)** — adaptive Accept-Encoding strip : plus de pages
|
||||
CSP-strict tirées décompressées via le worker R3 GIL-bound. **toolbox 2.6.53**.
|
||||
- ✅ **crowdsec** réparé (403 transitoire CDN → `dpkg --configure` RC=0, audit clean).
|
||||
|
||||
- ✅ **#623 (PR #648, merged 9950e9ec)** — clobber systémique RÉSOLU au source.
|
||||
La vraie cause : boilerplate scaffold `install -d -m 750 /var/lib/secubox` +
|
||||
`/run/secubox` (parents NUS) dans ~56 postinsts — écrit `-m 750` (3 chiffres),
|
||||
d'où le ratage des sweeps précédents. Empiriquement prouvé que le form
|
||||
`install -d -m 750 /parent/leaf` NE clobbe PAS le parent (seuls les targets
|
||||
parents-nus). Fix : tous → 1777 (/run) / 0755 ; 6 lignes multi-arg splittées
|
||||
(4 mettaient /var/lib en world-writable 1777) ; 3 `chmod 750 /var/log` ;
|
||||
scaffold `new-package.sh` + `PATTERNS.md` ; core 1.1.8 tmpfiles.d déclare les 5
|
||||
parents 0755. **PAS de mass-deploy** (60 paquets = mass-restart = risque
|
||||
thundering-herd) ; live couvert par `dirs-guard.timer` ; arrive au prochain
|
||||
build CI / reflash.
|
||||
|
||||
- ✅ **#649 Lever A — selective SNI-splice (PR #650, toolbox 2.6.54 LIVE dark)**.
|
||||
New `tls_splice` addon (first in mitm-wg chain) splices pure-asset flows at the
|
||||
TLS ClientHello — curated media seed (googlevideo/ytimg/fbcdn/twimg/scdn…) ∪
|
||||
autolearn-promoted never-HTML hosts — so GIL-bound R3 workers skip
|
||||
forge/decrypt/parse/16-addons on no-L7-value flows. Ships `tls_splice=observe`
|
||||
(DARK: classify+log, still MITM). Deployed gk2, addon loads clean, 0 runtime
|
||||
errors. Answer to "do we need full mitm?": YES for outbound HTTPS (per-host cert
|
||||
forging is intrinsic) — but only decrypt what we modify. Lever B (Go/Rust core)
|
||||
= strategic follow-up. WAF = later.
|
||||
|
||||
### ⬜ Next Up
|
||||
|
||||
- **#649 SOAK → FLIP** — review `would-splice` logs + `/run/secubox/splice.json`
|
||||
on real traffic for a soak window, confirm no first-party/HTML host is
|
||||
classified, then flip `tls_splice=on` in `/etc/secubox/toolbox/filters.json`
|
||||
(hot-reload). Before flip: the fortknox-via-WebUI refresh gap is already fixed.
|
||||
- **Lever B (#649 follow-up)** — Go/Rust forging-proxy core if A isn't enough.
|
||||
- **Anti-Track v2 ARMING** (décision USER, gated) — soak observe-only puis flip
|
||||
`privacy_enforce=true` ; régénérer `data/cdn-allowlist.txt` depuis les plages
|
||||
publiques avant `privacy_ip_drop` ; `unbound-checkconf` avant `privacy_dns_feed`.
|
||||
- **Tunnel R3 perf** — l'encoding fix aide ; reste la contention CPU board-wide
|
||||
(load ~5/4 cœurs, workers mono-thread). Lever suivant = réduire les co-tenants
|
||||
(gitea/R2-mitm/crowdsec/metrics) ou isoler le mitm, pas du tuning d'addon.
|
||||
- **#615** — Security Posture dans la navbar du Hub (petit enhancement).
|
||||
- **#592 webmail-hub** — BLOQUÉ : besoin client OAuth Google + vhost ; Phase 1
|
||||
IMAP (Gandi/OVH) peut démarrer sans OAuth.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 2026-06-14 : ToolBoX privacy/perf sprint — 2.6.36 live (see HISTORY)
|
||||
|
||||
Tout mergé + déployé sur gk2 (kbin sain, `secubox-toolbox 2.6.36`).
|
||||
Détail complet dans HISTORY 2026-06-14. Résumé :
|
||||
|
||||
- ✅ Protective spoof (#560), modular filters + ad-ghoster (#566, collapse
|
||||
#584), media cache opt-in (#577), autolearn (#589/#591), DPI media donut
|
||||
(#570), donut + domain-nugget cartographie (#553/#587, IP cachées #575,
|
||||
favicons #555), guirlande banner + pin (#572/#578), webext popup panel
|
||||
(#574), /ca/fingerprint R3 (#562), postinst restart fix (#581),
|
||||
detect_antibot deployment-vs-challenge (#564).
|
||||
- ✅ Clients : APK v0.3.0 (zero-tap launch+boot), webext v0.1.4.
|
||||
- ✅ Fixes live : Nextcloud iPhone photos (files_antivirus off + PHP
|
||||
limits), kbin 503 (#581).
|
||||
|
||||
### ⬜ Next Up
|
||||
|
||||
- **#592 secubox-webmail-hub** (Gmail OAuth2 + Gandi + OVH, inbox unifié) —
|
||||
design filé, **BLOQUÉ** : besoin d'un client OAuth Google (client_id/
|
||||
secret/redirect) + nom de vhost + (read-only Phase 1 ?). Phase 1 IMAP
|
||||
(Gandi/OVH) peut démarrer sans OAuth sur "start phase 1".
|
||||
- Côté user : re-trust R3 CA `D5:E4:3A` sur l'iPhone (bannière HTTPS) ;
|
||||
tester l'upload photo Nextcloud ; activer `media_cache` si voulu
|
||||
(`/admin/filters/ui`) et surveiller `/admin/cache`.
|
||||
*Mis à jour : 2026-06-13*
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -269,18 +21,11 @@ le navigateur : badge live des traceurs + popup.
|
|||
- **Serve depuis la toolbox** (`2.6.14`) : `GET /wg/toolbox.xpi` (local
|
||||
sinon 302 → release), bouton `🧩 Extension navigateur` sur les 2
|
||||
panneaux onboard, helper `secubox-toolbox-fetch-xpi`, postinst dir.
|
||||
- **CI** : `build-webext.yml` — `web-ext lint` (0 erreur, 2 warnings
|
||||
bénins) + build, artifact, release asset sur tag `webext-v*`.
|
||||
- **Release** (PR #540 + #541, mergées) : tag `webext-v0.1.1` poussé →
|
||||
CI a publié `secubox-toolbox-webext.xpi` (téléchargeable, vérifié 200).
|
||||
`make_latest:false` + URL **tag-pinned** dans `/wg/toolbox.xpi` +
|
||||
`secubox-toolbox-fetch-xpi` pour ne pas voler le pointeur "Latest" à la
|
||||
release APK Android (dont l'endpoint résout via `/releases/latest/...`).
|
||||
→ bumper le tag dans la constante + le helper à chaque `webext-v*`.
|
||||
- **CI** : `build-webext.yml` — `web-ext lint` + build, artifact, release
|
||||
asset `secubox-toolbox-webext.xpi` sur tag `webext-v*`.
|
||||
- **Reste à faire** : signature AMO (`.xpi` non signé = sideload/dev) ;
|
||||
endpoint SSE `/social/live/{token}` optionnel ; icône PNG Chromium ;
|
||||
contrôle Poke/Emancipate par-site quand #525 (déception) arrive ;
|
||||
déployer `secubox-toolbox 2.6.14` sur la board pour activer le bouton.
|
||||
contrôle Poke/Emancipate par-site quand #525 (déception) arrive.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
1
.github/workflows/build-all-live-usb.yml
vendored
|
|
@ -48,7 +48,6 @@ jobs:
|
|||
output_pattern: "secubox-live-amd64-*.img*"
|
||||
needs_qemu: false
|
||||
embed_image: false
|
||||
extra_args: "--kiosk"
|
||||
|
||||
# MOCHAbin (arm64) - U-Boot distroboot
|
||||
- platform: mochabin
|
||||
|
|
|
|||
25
.github/workflows/build-packages.yml
vendored
|
|
@ -63,11 +63,6 @@ jobs:
|
|||
# Build the flat {package, arch} matrix. Honour the workflow_dispatch
|
||||
# `arch` and `package` filters if set (empty on `push: tags` events).
|
||||
requested_arch="${REQUESTED_ARCH:-}"
|
||||
# `both` means build every arch — same as the empty (push: tags)
|
||||
# case. Without this the matrix filter (which only compares against
|
||||
# amd64/arm64/empty) yields an EMPTY matrix, so no package builds and
|
||||
# `collect` fails.
|
||||
[ "$requested_arch" = "both" ] && requested_arch=""
|
||||
requested_pkg="${REQUESTED_PKG:-}"
|
||||
|
||||
combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
|
||||
|
|
@ -157,12 +152,7 @@ jobs:
|
|||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
build-essential dpkg-dev debhelper devscripts fakeroot \
|
||||
dh-python python3-all python3-setuptools golang-go
|
||||
# golang-go satisfies Build-Depends of the pure-Go packages
|
||||
# (secubox-dpi, secubox-toolbox-ng: CGO_ENABLED=0, GOARCH=arm64,
|
||||
# -mod=vendor offline cross-compile). ubuntu-24.04 ships >= 1.22.
|
||||
# Without it dpkg-checkbuilddeps aborts the arm64 build — this was
|
||||
# the real cause of the "arm64 red" runs, not a CGO toolchain gap.
|
||||
dh-python python3-all python3-setuptools
|
||||
# arm64 cross-toolchain — dh_strip and dh_makeshlibs invoke
|
||||
# aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed.
|
||||
# Without these, arch-specific packages shipping prebuilt
|
||||
|
|
@ -223,18 +213,7 @@ jobs:
|
|||
# no-op; for arm64 jobs that don't compile native code (Python +
|
||||
# prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is
|
||||
# enough to cross-stamp the .deb.
|
||||
#
|
||||
# Pure-Go packages (CGO_ENABLED=0, GOARCH cross) only need the `go`
|
||||
# toolchain, which is present via golang-1.22-go. But their
|
||||
# `Build-Depends: golang-go (>= 1.22)` trips dpkg-checkbuilddeps
|
||||
# because apt registers golang-1.22-go, not the golang-go
|
||||
# metapackage, on the runner. Skip the dep check (-d) for just these
|
||||
# — the compiler is there and the build is self-contained (-mod=vendor).
|
||||
DEPFLAG=""
|
||||
case "${{ matrix.package }}" in
|
||||
secubox-dpi|secubox-toolbox-ng|secubox-waf-ng) DEPFLAG="-d" ;;
|
||||
esac
|
||||
dpkg-buildpackage -us -uc -b $DEPFLAG -a ${{ matrix.arch }}
|
||||
dpkg-buildpackage -us -uc -b -a ${{ matrix.arch }}
|
||||
|
||||
echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})"
|
||||
|
||||
|
|
|
|||
24
README.md
|
|
@ -57,30 +57,6 @@
|
|||
|
||||
---
|
||||
|
||||
## 🗡️ kbin — le premier outil du couteau suisse cyber
|
||||
|
||||
**kbin** (`kbin.gk2.secubox.in`) est le portail public de la **ToolBoX** SecuBox — la
|
||||
*cabine numérique* et **première lame du couteau suisse cyber modulaire** de
|
||||
[cybermind.fr](https://cybermind.fr). On s'y branche, on surfe normalement, et la lame
|
||||
inspecte et protège le trafic de façon transparente :
|
||||
|
||||
| 🗡️ | Lame |
|
||||
|----|------|
|
||||
| ⚡ | **Performance transparente** — on ne déchiffre que ce qu'on modifie (SNI-splice sélectif) |
|
||||
| 🔒 | **Full encrypted** — inspection MITM complète, forge de cert par hôte, fingerprint Chrome uTLS |
|
||||
| ☠️ | **Injection de poison & smog** — le trafic ad-tech ressort empoisonné, pas seulement bloqué |
|
||||
| 🚫 | **Bandeau anti-adware** — transparence injectée, immune au CSP, SPA-aware |
|
||||
| 🛡️ | **Safe browsing** — Vortex DNS + blacklist nft + détection anti-bot |
|
||||
|
||||
> **Prochaine lame — 🧅 mode Tor quick-switch ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).**
|
||||
> Un tap → le surf ressort par le réseau Tor (egress sortant, pseudo-network) : l'inspection
|
||||
> reste intacte, seule l'**IP de sortie** devient anonyme. Fail-closed, opt-in, sans fuite DNS.
|
||||
|
||||
- Use-case : [docs/wiki/Kbin-Toolbox.md](docs/wiki/Kbin-Toolbox.md)
|
||||
- FAQ : [docs/FAQ-KBIN-TOR.md](docs/FAQ-KBIN-TOR.md)
|
||||
|
||||
---
|
||||
|
||||
## License — CyberMind Source-Disclosed (CMSD-1.0)
|
||||
|
||||
> **Source disclosed, rights reserved.**
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ network:
|
|||
bridges:
|
||||
br-lan:
|
||||
interfaces: [lan0, lan1, lan2, lan3]
|
||||
addresses: [192.168.10.1/24]
|
||||
addresses: [192.168.1.1/24]
|
||||
dhcp4: false
|
||||
parameters:
|
||||
stp: false
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ network:
|
|||
bridges:
|
||||
br-lan:
|
||||
interfaces: [lan0, lan1]
|
||||
addresses: [192.168.10.1/24]
|
||||
addresses: [192.168.1.1/24]
|
||||
dhcp4: false
|
||||
parameters:
|
||||
stp: false
|
||||
|
|
|
|||
|
|
@ -6,21 +6,16 @@ network:
|
|||
renderer: networkd
|
||||
|
||||
ethernets:
|
||||
# WAN candidate (SFP+, eth0) — connecté à l'opérateur via fibre/module SFP.
|
||||
# WAN — connecté à l'opérateur
|
||||
eth0:
|
||||
dhcp4: true
|
||||
dhcp6: false
|
||||
optional: true
|
||||
|
||||
# LAN — port GbE switch (DSA 88E6341)
|
||||
# LAN — ports GbE (DSA ou directs selon la config switch)
|
||||
eth1:
|
||||
optional: true
|
||||
# WAN candidate (RJ45 cuivre, eth2 = mvpp2-2). Sur MOCHAbin le seul RJ45
|
||||
# direct ; sert d'uplink quand l'opérateur arrive en cuivre. Le port WAN
|
||||
# câblé (eth0 SFP+ OU eth2 cuivre) obtient le bail DHCP ; l'autre reste idle.
|
||||
eth2:
|
||||
dhcp4: true
|
||||
dhcp6: false
|
||||
optional: true
|
||||
eth3:
|
||||
optional: true
|
||||
|
|
@ -36,8 +31,8 @@ network:
|
|||
bridges:
|
||||
# Bridge LAN
|
||||
br-lan:
|
||||
interfaces: [eth1, eth3, eth4]
|
||||
addresses: [192.168.10.1/24]
|
||||
interfaces: [eth1, eth2, eth3, eth4]
|
||||
addresses: [192.168.1.1/24]
|
||||
dhcp4: false
|
||||
parameters:
|
||||
stp: false
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ network:
|
|||
|
||||
# LAN — Interface 2 QEMU (si configurée)
|
||||
enp0s2:
|
||||
addresses: [192.168.10.1/24]
|
||||
addresses: [192.168.100.1/24]
|
||||
dhcp4: false
|
||||
optional: true
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ network:
|
|||
|
||||
# LAN — Interface 2 VirtualBox (Internal Network ou Host-Only)
|
||||
enp0s8:
|
||||
addresses: [192.168.10.1/24]
|
||||
addresses: [192.168.100.1/24]
|
||||
dhcp4: false
|
||||
optional: true
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ network:
|
|||
br-lan:
|
||||
interfaces: []
|
||||
addresses:
|
||||
- 192.168.10.1/24
|
||||
- 192.168.1.1/24
|
||||
dhcp4: false
|
||||
optional: true
|
||||
parameters:
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ network:
|
|||
bridges:
|
||||
br-lan:
|
||||
interfaces: [enp0s8]
|
||||
addresses: [192.168.10.1/24]
|
||||
addresses: [192.168.1.1/24]
|
||||
dhcp4: false
|
||||
parameters:
|
||||
stp: false
|
||||
|
|
|
|||
|
|
@ -12,20 +12,10 @@ import the WireGuard profile, verify the tunnel, then open the live
|
|||
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
|
||||
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
||||
|
||||
## Root path — real zero-tap, fully automated (#538, #551, #558)
|
||||
On a **rooted** device the app onboards with **zero taps**, two ways:
|
||||
|
||||
- **On launch** — auto-detects root and runs the silent sequence immediately
|
||||
every launch (no gate), retrying reachability while WiFi/tunnel settle.
|
||||
- **On boot** — a `BOOT_COMPLETED` receiver starts a short foreground service
|
||||
(`OnboardService`) that runs the same silent sequence **without opening the
|
||||
app**, then stops. After one reboot the device self-onboards.
|
||||
|
||||
The **⚡ Installation automatique (root)** button remains as a manual
|
||||
re-trigger. Two interactions are **mandated by Android and unavoidable** for
|
||||
any app: the sideload install confirm ("install unknown apps") and the
|
||||
first-time superuser (Magisk/su) grant prompt. Everything after those is
|
||||
zero-tap. Steps:
|
||||
## Root path — fully-automated silent onboarding (#538)
|
||||
When the device is **rooted**, the Discover step shows an extra
|
||||
**⚡ Installation automatique (root)** button. Tapping it runs the whole
|
||||
onboarding with zero further interaction (`RootAuto` step, streaming log):
|
||||
|
||||
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
||||
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId = "in.secubox.toolbox"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 4
|
||||
versionName = "0.4.0"
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- #558 full-auto: run the silent onboarding on device boot, no app open. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- Query the WireGuard app so we can hand it the generated profile. -->
|
||||
<queries>
|
||||
<package android:name="com.wireguard.android" />
|
||||
|
|
@ -31,26 +26,6 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- #558 : boot-completed → start the onboarding foreground service
|
||||
so a rooted device self-onboards with zero taps after a reboot. -->
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:exported="true"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name=".OnboardService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Silent R3 onboarding on a rooted, operator-owned cabine device" />
|
||||
</service>
|
||||
|
||||
<!-- FileProvider to share the downloaded CA + WG .conf with the
|
||||
system cert installer / the WireGuard app. -->
|
||||
<provider
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
//
|
||||
// #558 — boot-completed → kick the onboarding foreground service so a
|
||||
// rooted, operator-owned cabine device self-onboards with zero taps after
|
||||
// a reboot (no need to open the app).
|
||||
package `in`.secubox.toolbox
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val a = intent?.action ?: return
|
||||
if (a == Intent.ACTION_BOOT_COMPLETED || a == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
|
||||
ContextCompat.startForegroundService(
|
||||
context, Intent(context, OnboardService::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -64,69 +64,8 @@ fun OnboardApp() {
|
|||
val api = remember(host) { ToolboxApi(host) }
|
||||
var rootAvail by remember { mutableStateOf(false) }
|
||||
val rootLog = remember { mutableStateListOf<String>() }
|
||||
val prefs = remember {
|
||||
ctx.getSharedPreferences("secubox-toolbox", android.content.Context.MODE_PRIVATE)
|
||||
}
|
||||
var autoTried by remember { mutableStateOf(false) }
|
||||
|
||||
// The whole root-mode silent run, reused by the ⚡ button AND the
|
||||
// zero-tap auto-launch (#551/#558). NO onboarded gate — it auto-runs
|
||||
// every launch (idempotent: re-asserts CA + WG). Reachability is
|
||||
// RETRIED so a WiFi/tunnel race at launch doesn't kill the auto-run.
|
||||
val runRootAuto: () -> Unit = {
|
||||
busy = true; status = ""; rootLog.clear()
|
||||
scope.launch {
|
||||
// poll reachability up to ~9 s (network may still be settling)
|
||||
var ok = false
|
||||
for (attempt in 1..6) {
|
||||
ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||
if (ok) break
|
||||
status = "Recherche de la borne… ($attempt)"
|
||||
kotlinx.coroutines.delay(1500)
|
||||
}
|
||||
if (!ok) {
|
||||
busy = false; status = "Borne injoignable — vérifie le réseau."
|
||||
} else {
|
||||
step = Step.RootAuto
|
||||
val onb = RootOnboard(api, ctx.cacheDir, ctx.filesDir)
|
||||
val out = withContext(Dispatchers.IO) {
|
||||
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
|
||||
}
|
||||
busy = false
|
||||
onTunnel = out.verified
|
||||
// #683 — surface kbin Tor egress status (anonymised exit) if on.
|
||||
rootLog.add(withContext(Dispatchers.IO) {
|
||||
val t = api.torStatus()
|
||||
when {
|
||||
t == null -> "• Statut Tor : indisponible"
|
||||
!t.optBoolean("tor_mode", false) -> "• Mode Tor : inactif"
|
||||
t.optBoolean("running", false) ->
|
||||
"🧅 Mode Tor ACTIF — sortie anonymisée${t.optString("exit_ip", "").let { if (it.isNotBlank() && it != "null") " ($it)" else "" }}"
|
||||
else -> "🧅 Mode Tor activé — tunnel Tor en démarrage…"
|
||||
}
|
||||
})
|
||||
when {
|
||||
out.verified -> step = Step.Done
|
||||
out.wgViaApp -> { step = Step.ImportProfile
|
||||
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
|
||||
else -> { step = Step.Verify
|
||||
status = "Active le tunnel puis vérifie." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect root once, off the main thread.
|
||||
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
|
||||
// Zero-tap (#558): on a rooted device, auto-run the silent onboarding
|
||||
// on every launch — no gate. (Boot-time auto-run is handled by
|
||||
// BootReceiver + OnboardService so it runs without opening the app.)
|
||||
LaunchedEffect(rootAvail) {
|
||||
if (rootAvail && !autoTried && step == Step.Discover) {
|
||||
autoTried = true
|
||||
runRootAuto()
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(colorScheme = darkColorScheme(
|
||||
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
||||
|
|
@ -166,11 +105,32 @@ fun OnboardApp() {
|
|||
}
|
||||
if (rootAvail) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("🔓 Root détecté — l'installation se lance automatiquement. " +
|
||||
"Tu peux aussi la relancer ici.",
|
||||
Text("🔓 Root détecté — installation 100% automatique possible.",
|
||||
color = Matrix, fontSize = 12.sp)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
OutlinedButton(onClick = runRootAuto, modifier = Modifier.fillMaxWidth(),
|
||||
OutlinedButton(onClick = {
|
||||
busy = true; status = ""; rootLog.clear()
|
||||
scope.launch {
|
||||
val ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||
if (!ok) { busy = false; status = "Borne injoignable."; return@launch }
|
||||
step = Step.RootAuto
|
||||
val onb = RootOnboard(api, ctx.cacheDir)
|
||||
val out = withContext(Dispatchers.IO) {
|
||||
onb.runSilent { line ->
|
||||
scope.launch(Dispatchers.Main) { rootLog.add(line) }
|
||||
}
|
||||
}
|
||||
busy = false
|
||||
onTunnel = out.verified
|
||||
when {
|
||||
out.verified -> step = Step.Done
|
||||
out.wgViaApp -> { step = Step.ImportProfile
|
||||
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
|
||||
else -> { step = Step.Verify
|
||||
status = "Active le tunnel puis vérifie." }
|
||||
}
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth(),
|
||||
border = BorderStroke(1.dp, Matrix),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) {
|
||||
Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold)
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
//
|
||||
// #558 — full-auto onboarding service. Started on boot (BootReceiver). On a
|
||||
// rooted device it runs the silent R3 onboarding (system CA + native WG +
|
||||
// verify) with zero taps, retrying reachability while the network settles,
|
||||
// then stops itself. Non-root / unreachable → it just stops (the launcher
|
||||
// activity remains the manual path).
|
||||
package `in`.secubox.toolbox
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class OnboardService : Service() {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val CHAN = "sbx-onboard"
|
||||
private val NID = 4201
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startForeground(NID, buildNotification())
|
||||
scope.launch {
|
||||
try { runOnce() } finally { stopSelf() }
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private suspend fun runOnce() {
|
||||
// root is the precondition for the silent path; bail quietly otherwise.
|
||||
if (!RootShell.available()) return
|
||||
val host = getSharedPreferences("secubox-toolbox", Context.MODE_PRIVATE)
|
||||
.getString("host", null) ?: "kbin.gk2.secubox.in"
|
||||
val api = ToolboxApi(host)
|
||||
// network may still be coming up after boot — retry ~30 s.
|
||||
var ok = false
|
||||
for (i in 1..15) {
|
||||
ok = api.reachable()
|
||||
if (ok) break
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (!ok) return
|
||||
RootOnboard(api, cacheDir, filesDir).runSilent { /* headless: no UI log */ }
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
nm?.createNotificationChannel(
|
||||
NotificationChannel(CHAN, "SecuBox onboarding",
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
)
|
||||
}
|
||||
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
Notification.Builder(this, CHAN) else @Suppress("DEPRECATION") Notification.Builder(this)
|
||||
return b.setContentTitle("VILLAGE3B")
|
||||
.setContentText("Activation R3 automatique…")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,14 +9,7 @@ import java.io.File
|
|||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
|
||||
class RootOnboard(
|
||||
private val api: ToolboxApi,
|
||||
private val cacheDir: File,
|
||||
// #683: app-internal storage for the STABLE WG identity (survives reboot).
|
||||
// Defaults to cacheDir so older call sites still compile, but real callers
|
||||
// pass filesDir so the identity persists instead of churning each boot.
|
||||
private val filesDir: File = cacheDir,
|
||||
) {
|
||||
class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
|
||||
|
||||
/** A line appended to the on-screen log during the silent run. */
|
||||
fun interface Logger { fun log(line: String) }
|
||||
|
|
@ -130,10 +123,8 @@ class RootOnboard(
|
|||
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
|
||||
return false
|
||||
}
|
||||
log.log("• Profil WireGuard (identité stable)…")
|
||||
// #683: reuse the persisted keypair so the device keeps ONE identity
|
||||
// across reboots (no more stats reset to a fresh empty hash each boot).
|
||||
val conf = api.persistentProfile(filesDir).readText()
|
||||
log.log("• Génération du profil WireGuard…")
|
||||
val conf = api.downloadProfile(cacheDir).readText()
|
||||
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
|
||||
val iface = "wg-village3b"
|
||||
val r = RootShell.runScript(
|
||||
|
|
|
|||
|
|
@ -51,41 +51,6 @@ class ToolboxApi(rawHost: String) {
|
|||
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
|
||||
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
|
||||
|
||||
/**
|
||||
* The device's STABLE WireGuard identity (#683 lost-referrer fix).
|
||||
*
|
||||
* `/wg/profile/new` mints a FRESH keypair on every call. The onboarding
|
||||
* runs on every boot, so calling it each time gave the device a NEW pubkey
|
||||
* → new sha256(pubkey) identity hash → its stats/social history reset to an
|
||||
* empty bucket on every reboot/reconnect. Here we fetch a peer ONCE and
|
||||
* persist the .conf in app-internal `filesDir` (survives reboots, unlike the
|
||||
* evictable cacheDir). Every later call reuses the SAME keypair → SAME
|
||||
* identity → the device keeps one continuous history.
|
||||
*
|
||||
* Survives reboot/reconnect/app-restart. (Reinstall still wipes filesDir;
|
||||
* cross-reinstall persistence would need allowBackup — kept off for CSPN.)
|
||||
*/
|
||||
fun persistentProfile(filesDir: File): File {
|
||||
val stored = File(filesDir, "identity-wg.conf")
|
||||
if (stored.exists() && stored.length() > 0L &&
|
||||
stored.readText().contains("PrivateKey", ignoreCase = true)) {
|
||||
return stored
|
||||
}
|
||||
val fresh = download("/wg/profile/new", "identity-wg.conf.tmp", filesDir)
|
||||
fresh.copyTo(stored, overwrite = true)
|
||||
fresh.delete()
|
||||
return stored
|
||||
}
|
||||
|
||||
/** kbin Tor egress status for the client UI (read-only, kbin-safe). */
|
||||
fun torStatus(): JSONObject? {
|
||||
val c = open("/wg/tor-status")
|
||||
return try {
|
||||
if (c.responseCode !in 200..299) null
|
||||
else JSONObject(c.inputStream.bufferedReader().readText())
|
||||
} catch (_: Exception) { null } finally { c.disconnect() }
|
||||
}
|
||||
|
||||
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
|
||||
fun r3Check(): Pair<Boolean, String?> {
|
||||
val c = open("/wg/r3-check")
|
||||
|
|
|
|||
|
|
@ -28,13 +28,7 @@ to your cabine over the R3 tunnel — no third-party calls.
|
|||
|
||||
## Install
|
||||
|
||||
Published release `.xpi` (downloadable directly):
|
||||
|
||||
```
|
||||
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi
|
||||
```
|
||||
|
||||
The toolbox also serves it from the cabine:
|
||||
The toolbox serves the built extension:
|
||||
|
||||
```
|
||||
https://kbin.<board>.secubox.in/wg/toolbox.xpi
|
||||
|
|
@ -42,26 +36,15 @@ https://kbin.<board>.secubox.in/wg/toolbox.xpi
|
|||
|
||||
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
|
||||
button. When a local build is present the cabine serves it; otherwise it
|
||||
302-redirects to the **tag-pinned** release asset above. The webext release
|
||||
is published `make_latest:false` so it does not steal the repo "Latest"
|
||||
pointer from the Android APK release (whose endpoint resolves via
|
||||
`/releases/latest/download/…`) — bump the tag in the `/wg/toolbox.xpi`
|
||||
endpoint constant + `secubox-toolbox-fetch-xpi` when a new `webext-v*`
|
||||
release is cut.
|
||||
302-redirects to the latest GitHub release asset `secubox-toolbox-webext.xpi`.
|
||||
|
||||
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed
|
||||
build (release CI step / `web-ext sign`); for development use
|
||||
*about:debugging → Load Temporary Add-on*, or an ESR/Dev build with
|
||||
`xpinstall.signatures.required=false`.
|
||||
- **Linux Firefox (fast)** — one call grabs the `.xpi` and launches Firefox
|
||||
with it loaded (via `web-ext run`, no signing needed):
|
||||
```bash
|
||||
clients/webext-toolbox/install-firefox-linux.sh # from kbin.gk2.secubox.in
|
||||
clients/webext-toolbox/install-firefox-linux.sh --release # from the GitHub release
|
||||
clients/webext-toolbox/install-firefox-linux.sh --local # from this checkout
|
||||
```
|
||||
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
|
||||
Ships rasterised PNG icons (`icons/icon-48/128.png`), so it loads as-is.
|
||||
Note: Chromium action icons must be raster — rasterise `icons/icon.svg`
|
||||
to PNG before a Chrome Web Store build (Firefox accepts the SVG as-is).
|
||||
|
||||
## Build
|
||||
|
||||
|
|
|
|||
|
|
@ -65,17 +65,6 @@ async function r3Check(host) {
|
|||
}
|
||||
}
|
||||
|
||||
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
|
||||
async function torStatus(host) {
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
|
||||
if (!resp.ok) return { tor_mode: false };
|
||||
return await resp.json();
|
||||
} catch (_) {
|
||||
return { tor_mode: false };
|
||||
}
|
||||
}
|
||||
|
||||
// graph: the per-session cartographie JSON. Throws on HTTP error so the
|
||||
// caller can show "token expired — re-pair".
|
||||
async function graph(host, token, since) {
|
||||
|
|
@ -100,35 +89,6 @@ async function wipe(host, token) {
|
|||
return await resp.json();
|
||||
}
|
||||
|
||||
// #574 — protection stats + modular filter toggles (cabine admin API).
|
||||
async function ghost(host) {
|
||||
try {
|
||||
const r = await fetch(`${baseUrl(host)}/admin/ghost`, { credentials: "omit" });
|
||||
return r.ok ? await r.json() : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
async function getAdminFilters(host) {
|
||||
try {
|
||||
const r = await fetch(`${baseUrl(host)}/admin/filters`, { credentials: "omit" });
|
||||
return r.ok ? await r.json() : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
async function setAdminFilters(host, patch) {
|
||||
const r = await fetch(`${baseUrl(host)}/admin/filters`, {
|
||||
method: "POST", credentials: "omit",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
// Favicon of a major site/tracker via the cabine's server-side proxy
|
||||
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
|
||||
function faviconUrl(host, domain) {
|
||||
return `${baseUrl(host)}/social/favicon/${encodeURIComponent(domain || "")}`;
|
||||
}
|
||||
|
||||
function socialUrl(host, token) {
|
||||
return `${baseUrl(host)}/social/${token}`;
|
||||
}
|
||||
|
|
@ -144,13 +104,8 @@ const SbxApi = {
|
|||
setConfig,
|
||||
pair,
|
||||
r3Check,
|
||||
torStatus,
|
||||
graph,
|
||||
wipe,
|
||||
ghost,
|
||||
getAdminFilters,
|
||||
setAdminFilters,
|
||||
faviconUrl,
|
||||
socialUrl,
|
||||
reportUrl,
|
||||
tokenFromUrl,
|
||||
|
|
|
|||
|
|
@ -12,18 +12,15 @@ if (typeof importScripts === "function") {
|
|||
try { importScripts("api.js"); } catch (_) {}
|
||||
}
|
||||
|
||||
// NB: do NOT declare `const ext` here — api.js already declares it in the
|
||||
// shared script scope (event page) / worker global (importScripts), and a
|
||||
// second `const ext` is a "redeclaration of const ext" SyntaxError that
|
||||
// kills the whole background script. Use api.ext instead.
|
||||
const api = globalThis.SbxApi;
|
||||
const ext = globalThis.browser || globalThis.chrome;
|
||||
|
||||
const ALARM = "sbx-refresh";
|
||||
const PERIOD_MIN = 1; // poll the cabine once a minute
|
||||
|
||||
function setBadge(text, color) {
|
||||
try {
|
||||
api.ext.action.setBadgeText({ text: text || "" });
|
||||
ext.action.setBadgeText({ text: text || "" });
|
||||
if (color) ext.action.setBadgeBackgroundColor({ color });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
|
@ -62,18 +59,18 @@ async function refresh() {
|
|||
}
|
||||
}
|
||||
|
||||
api.ext.runtime.onInstalled.addListener(() => {
|
||||
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||
ext.runtime.onInstalled.addListener(() => {
|
||||
ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||
refresh();
|
||||
});
|
||||
api.ext.runtime.onStartup && api.ext.runtime.onStartup.addListener(() => {
|
||||
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||
ext.runtime.onStartup && ext.runtime.onStartup.addListener(() => {
|
||||
ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||
refresh();
|
||||
});
|
||||
api.ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
|
||||
ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
|
||||
|
||||
// popup asks for an immediate refresh after pairing / config change
|
||||
api.ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg && msg.type === "refresh") {
|
||||
refresh().then(() => sendResponse({ ok: true }));
|
||||
return true; // async response
|
||||
|
|
|
|||
|
|
@ -14,11 +14,9 @@ OUT="secubox-toolbox-webext-${VER}.xpi"
|
|||
rm -f "$OUT"
|
||||
|
||||
# -FS = sync (drop stale entries) ; exclude VCS, dotfiles, build script,
|
||||
# any previously built artefact, docs, and the SVG icon source (only the
|
||||
# rasterised PNGs are referenced by the manifest — keep SVG out of the
|
||||
# package so Firefox never renders it in chrome UI).
|
||||
# any previously built artefact, and docs.
|
||||
zip -r -FS "$OUT" . \
|
||||
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' 'icons/icon.svg' >/dev/null
|
||||
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' >/dev/null
|
||||
|
||||
echo "built $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)"
|
||||
echo "Firefox: about:debugging → This Firefox → Load Temporary Add-on → pick the .xpi (or manifest.json)."
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 618 B |
|
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
#
|
||||
# SecuBox ToolBoX — Linux Firefox installer (#547)
|
||||
# One call: grab the ToolBoX cartographie extension and launch Firefox with
|
||||
# it loaded. Prefers `web-ext run` (temporary load, works unsigned — fastest)
|
||||
# and falls back to opening the .xpi for the install prompt.
|
||||
#
|
||||
# Usage:
|
||||
# ./install-firefox-linux.sh # from kbin.gk2.secubox.in
|
||||
# ./install-firefox-linux.sh kbin.my.box # from another cabine host
|
||||
# ./install-firefox-linux.sh --release # from the latest GitHub release
|
||||
# ./install-firefox-linux.sh --local # build from this checkout (web-ext)
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_HOST="kbin.gk2.secubox.in"
|
||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi"
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
||||
warn(){ printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
|
||||
die(){ printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── resolve source ──
|
||||
MODE="host"; HOST="$DEFAULT_HOST"; SRC_URL=""
|
||||
case "${1:-}" in
|
||||
--release) MODE="release"; SRC_URL="$RELEASE_URL" ;;
|
||||
--local) MODE="local" ;;
|
||||
"") SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
||||
-*) die "unknown flag: $1 (use --release | --local | <host>)" ;;
|
||||
*) HOST="$1"; SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
||||
esac
|
||||
|
||||
# ── find a Firefox binary ──
|
||||
FX=""
|
||||
for c in firefox firefox-esr firefox-bin firefox-developer-edition; do
|
||||
if command -v "$c" >/dev/null 2>&1; then FX="$c"; break; fi
|
||||
done
|
||||
if [ -z "$FX" ] && command -v flatpak >/dev/null 2>&1 \
|
||||
&& flatpak info org.mozilla.firefox >/dev/null 2>&1; then
|
||||
FX="flatpak run org.mozilla.firefox"
|
||||
fi
|
||||
[ -n "$FX" ] || die "no Firefox found (install firefox / firefox-esr, or flatpak org.mozilla.firefox)"
|
||||
say "Firefox: $FX"
|
||||
|
||||
have_webext(){ command -v web-ext >/dev/null 2>&1 || command -v npx >/dev/null 2>&1; }
|
||||
runwebext(){ if command -v web-ext >/dev/null 2>&1; then web-ext "$@"; else npx --yes web-ext "$@"; fi; }
|
||||
|
||||
# ── fastest path: web-ext run (temporary load, no signing needed) ──
|
||||
if have_webext; then
|
||||
SRCDIR=""
|
||||
if [ "$MODE" = "local" ]; then
|
||||
[ -f "$SELF_DIR/manifest.json" ] || die "--local: no manifest.json next to this script"
|
||||
SRCDIR="$SELF_DIR"
|
||||
else
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
say "Downloading extension from ${SRC_URL} …"
|
||||
curl -fsSL "$SRC_URL" -o "$TMP/sbx.xpi" || die "download failed: $SRC_URL"
|
||||
head -c2 "$TMP/sbx.xpi" | grep -q PK || die "not a valid .xpi (zip) — wrong host/URL?"
|
||||
mkdir -p "$TMP/ext" && ( cd "$TMP/ext" && unzip -q "$TMP/sbx.xpi" )
|
||||
SRCDIR="$TMP/ext"
|
||||
fi
|
||||
say "Launching Firefox with the ToolBoX extension loaded (temporary)…"
|
||||
FXBIN="${FX%% *}" # web-ext wants the binary, not a flatpak wrapper
|
||||
if [ "$FX" = "${FX# }" ] && command -v "$FXBIN" >/dev/null 2>&1; then
|
||||
exec runwebext run --source-dir "$SRCDIR" --firefox "$FXBIN" \
|
||||
--start-url "https://${HOST}/social/me"
|
||||
fi
|
||||
exec runwebext run --source-dir "$SRCDIR" --start-url "https://${HOST}/social/me"
|
||||
fi
|
||||
|
||||
# ── fallback: open the .xpi so Firefox shows the install prompt ──
|
||||
warn "web-ext not found (no npx) — falling back to the install prompt."
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
[ "$MODE" = "local" ] && die "--local needs web-ext/npx; install nodejs or use a host/--release"
|
||||
say "Downloading ${SRC_URL} …"
|
||||
curl -fsSL "$SRC_URL" -o "$TMP/secubox-toolbox-webext.xpi" || die "download failed"
|
||||
head -c2 "$TMP/secubox-toolbox-webext.xpi" | grep -q PK || die "not a valid .xpi"
|
||||
cat <<'NOTE'
|
||||
! The .xpi is unsigned. Stock Firefox release refuses a permanent install.
|
||||
Use Firefox ESR/Developer/Nightly, or set in about:config:
|
||||
xpinstall.signatures.required = false
|
||||
…then accept the install prompt that opens now.
|
||||
NOTE
|
||||
say "Opening Firefox on the extension…"
|
||||
exec $FX "$TMP/secubox-toolbox-webext.xpi"
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "SecuBox ToolBoX — Cartographie sociale",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.0",
|
||||
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
"default_title": "SecuBox Cartographie",
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": {
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
"48": "icons/icon.svg",
|
||||
"128": "icons/icon.svg"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
"48": "icons/icon.svg",
|
||||
"128": "icons/icon.svg"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
|
|
|
|||
|
|
@ -66,17 +66,8 @@ button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
|
|||
display: flex; align-items: center; gap: 6px; padding: 3px 2px;
|
||||
border-bottom: 1px solid #1a1a22; font-size: 11px;
|
||||
}
|
||||
.row .fav { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; background: #1a1a22; object-fit: contain; }
|
||||
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.row .hits { color: var(--muted); }
|
||||
|
||||
/* #574 — protection panel */
|
||||
#protect { margin: 8px 0; padding: 8px; background: #0e0e15; border: 1px solid #222; border-radius: 8px; }
|
||||
.phead { color: var(--matrix); font-weight: 700; font-size: 12px; margin-bottom: 6px; }
|
||||
.gstat { color: var(--muted); font-weight: 400; font-size: 10px; }
|
||||
.tg { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 3px 0; }
|
||||
.tg select { margin-left: auto; background: #14141c; color: var(--text); border: 1px solid #333; border-radius: 4px; }
|
||||
#protect input { accent-color: var(--void); }
|
||||
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
||||
.tier.cdn { background: #1d2a33; color: var(--cyan); }
|
||||
.tier.ab { background: #2a1416; color: var(--cinnabar); }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
<body>
|
||||
<header>
|
||||
<span class="logo">👁️ VILLAGE3B</span>
|
||||
<span id="tordot" class="r3 off" title="Mode Tor" style="display:none">🧅</span>
|
||||
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
|
||||
</header>
|
||||
|
||||
|
|
@ -37,17 +36,6 @@
|
|||
|
||||
<div class="toplist" id="topList"></div>
|
||||
|
||||
<section id="protect">
|
||||
<div class="phead">🛡 Protection <span id="ghostStat" class="gstat"></span></div>
|
||||
<label class="tg"><input type="checkbox" data-f="ad_ghost"> Masquer pubs/bannières (R3+)</label>
|
||||
<label class="tg"><input type="checkbox" data-f="ad_ghost_block"> Bloquer hôtes pub (économie)</label>
|
||||
<label class="tg"><input type="checkbox" data-f="banner"> Bannière transparence</label>
|
||||
<label class="tg">Mode protecteur
|
||||
<select data-f="protective"><option value="off">off</option><option value="alert">alert</option><option value="spoof">spoof</option></select>
|
||||
</label>
|
||||
<p id="protectMsg" class="muted"></p>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<button id="openFull">🗺️ Cartographie complète</button>
|
||||
<button id="pdf">📄 Rapport PDF</button>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@
|
|||
//
|
||||
// SecuBox-Deb :: webext-toolbox :: popup controller
|
||||
|
||||
// NB: api.js (loaded first in this page) already declares `const ext` in the
|
||||
// shared script scope — re-declaring it here is a "redeclaration of const ext"
|
||||
// SyntaxError that aborts popup.js. Use api.ext instead.
|
||||
const api = globalThis.SbxApi;
|
||||
const ext = api.ext;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let curHost = api.DEFAULTS.host; // for favicon URLs (#555)
|
||||
|
||||
function show(which) {
|
||||
$("pair").hidden = which !== "pair";
|
||||
|
|
@ -21,18 +18,10 @@ function fillTopList(nodes) {
|
|||
(nodes || [])
|
||||
.slice()
|
||||
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
||||
.slice(0, 5)
|
||||
.slice(0, 12)
|
||||
.forEach((n) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "row";
|
||||
// favicon of the major site/tracker (cabine proxy) — not an IP (#555)
|
||||
const fav = document.createElement("img");
|
||||
fav.className = "fav";
|
||||
fav.loading = "lazy";
|
||||
fav.alt = "";
|
||||
fav.src = api.faviconUrl(curHost, n.domain || n.id);
|
||||
fav.addEventListener("error", () => { fav.style.visibility = "hidden"; });
|
||||
row.appendChild(fav);
|
||||
const dom = document.createElement("span");
|
||||
dom.className = "dom";
|
||||
dom.textContent = n.domain || n.id;
|
||||
|
|
@ -64,45 +53,9 @@ function paint(data) {
|
|||
fillTopList(data.nodes);
|
||||
}
|
||||
|
||||
// #574 — protection stats + live filter toggles in the popup.
|
||||
async function loadProtection() {
|
||||
const sec = $("protect");
|
||||
if (!sec) return;
|
||||
const g = await api.ghost(curHost);
|
||||
if (g) {
|
||||
$("ghostStat").textContent =
|
||||
`${g.blocked_requests || 0} bloqués · ~${g.mb_saved_est || 0} Mo · ${g.pages_cleaned || 0} nettoyées`;
|
||||
}
|
||||
const f = await api.getAdminFilters(curHost);
|
||||
if (!f) { sec.style.opacity = "0.5"; return; }
|
||||
sec.style.opacity = "1";
|
||||
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||
const k = el.dataset.f;
|
||||
if (el.type === "checkbox") el.checked = !!f[k];
|
||||
else el.value = f[k];
|
||||
});
|
||||
if (!sec.dataset.wired) {
|
||||
sec.dataset.wired = "1";
|
||||
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||
el.addEventListener("change", async () => {
|
||||
const v = el.type === "checkbox" ? el.checked : el.value;
|
||||
try {
|
||||
await api.setAdminFilters(curHost, { [el.dataset.f]: v });
|
||||
$("protectMsg").textContent = "✓ appliqué";
|
||||
setTimeout(() => ($("protectMsg").textContent = ""), 1000);
|
||||
loadProtection();
|
||||
} catch (e) {
|
||||
$("protectMsg").textContent = "erreur : " + e.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const cfg = await api.getConfig();
|
||||
curHost = cfg.host || api.DEFAULTS.host;
|
||||
$("ver").textContent = "v" + (api.ext.runtime.getManifest().version || "");
|
||||
$("ver").textContent = "v" + (ext.runtime.getManifest().version || "");
|
||||
|
||||
// tunnel indicator
|
||||
api.r3Check(cfg.host).then((r) => {
|
||||
|
|
@ -111,21 +64,6 @@ async function load() {
|
|||
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
|
||||
});
|
||||
|
||||
// #683 — Tor egress indicator (only visible when kbin Tor mode is on)
|
||||
api.torStatus(cfg.host).then((t) => {
|
||||
const dot = $("tordot");
|
||||
if (!dot) return;
|
||||
if (t && t.tor_mode) {
|
||||
dot.style.display = "";
|
||||
dot.className = "r3 " + (t.running ? "on" : "off");
|
||||
dot.title = t.running
|
||||
? `Mode Tor actif — sortie anonymisée${t.exit_ip ? " (" + t.exit_ip + ")" : ""}`
|
||||
: "Mode Tor activé — démarrage du tunnel…";
|
||||
} else {
|
||||
dot.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
if (!cfg.token) {
|
||||
$("host").value = cfg.host;
|
||||
show("pair");
|
||||
|
|
@ -137,7 +75,6 @@ async function load() {
|
|||
const data = await api.graph(cfg.host, cfg.token, cfg.since);
|
||||
paint(data);
|
||||
$("liveMsg").textContent = "";
|
||||
loadProtection();
|
||||
} catch (e) {
|
||||
if (String(e.message) === "token-expired") {
|
||||
// token died — drop it and go back to pairing
|
||||
|
|
@ -159,7 +96,7 @@ $("pairBtn").addEventListener("click", async () => {
|
|||
await api.setConfig({ host });
|
||||
const token = await api.pair(host);
|
||||
await api.setConfig({ token });
|
||||
api.ext.runtime.sendMessage({ type: "refresh" });
|
||||
ext.runtime.sendMessage({ type: "refresh" });
|
||||
await load();
|
||||
} catch (e) {
|
||||
$("pairMsg").textContent = e.message + " (es-tu sur le tunnel ?)";
|
||||
|
|
@ -168,11 +105,11 @@ $("pairBtn").addEventListener("click", async () => {
|
|||
|
||||
$("openFull").addEventListener("click", async () => {
|
||||
const cfg = await api.getConfig();
|
||||
api.ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
|
||||
ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
|
||||
});
|
||||
$("pdf").addEventListener("click", async () => {
|
||||
const cfg = await api.getConfig();
|
||||
api.ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
|
||||
ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
|
||||
});
|
||||
$("wipe").addEventListener("click", async () => {
|
||||
if (!confirm("Effacer toutes tes données de cartographie sur la cabine ?")) return;
|
||||
|
|
@ -188,7 +125,7 @@ $("wipe").addEventListener("click", async () => {
|
|||
});
|
||||
$("settings").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
api.ext.runtime.openOptionsPage();
|
||||
ext.runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
load();
|
||||
|
|
|
|||
|
|
@ -55,13 +55,4 @@ server {
|
|||
proxy_pass http://unix:/run/secubox/system.sock:/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
|
||||
# #65: per-module routes self-register here. Every module package drops a
|
||||
# /etc/nginx/secubox-routes.d/<module>.conf (location-only snippet) at
|
||||
# install time, so a newly added module's /<module>/ + /api/v1/<module>/
|
||||
# routes are picked up automatically — no more hand-editing this file per
|
||||
# module. This is the ACTIVE include (matches the deployed webui.conf).
|
||||
# The crowdsec/waf/system blocks above stay hardcoded: those core packages
|
||||
# only ship the legacy secubox.d/ snippet, so they would NOT duplicate here.
|
||||
include /etc/nginx/secubox-routes.d/*.conf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
# AI Handover — prompt Mistral.ai (reprise du code + analyse projet)
|
||||
|
||||
Prompt prêt à coller dans **Mistral Le Chat** (ou via l'API) pour qu'un agent
|
||||
reprenne le code SecuBox-Deb et analyse le projet.
|
||||
|
||||
**Usage :** Le Chat n'a pas accès au dépôt ni au board `gk2` par défaut. Pour une
|
||||
vraie reprise, lance l'agent dans un IDE/agent ayant accès au filesystem + SSH,
|
||||
ou colle-lui `CLAUDE.md` + `.claude/*` en contexte. Mets à jour la section
|
||||
« ÉTAT ACTUEL » depuis `.claude/HISTORY.md` avant chaque réutilisation.
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
# RÔLE
|
||||
Tu es un ingénieur senior Debian / Python / sécurité réseau qui REPREND le projet
|
||||
SecuBox-Deb. Tu travailles méthodiquement : tu LIS avant d'écrire, tu vérifies
|
||||
avant d'affirmer, tu respectes à la lettre les conventions ci-dessous, et tu
|
||||
n'inventes pas de fichiers/commandes — tu les vérifies dans le dépôt. Langue : français.
|
||||
|
||||
# CONTEXTE PROJET
|
||||
SecuBox-Deb = plateforme cybersécurité CyberMind, portage Debian 12 (Bookworm)
|
||||
ARM64 depuis OpenWrt, cible ANSSI CSPN. Matériel : MOCHAbin / ESPRESSObin
|
||||
(Marvell Armada, aarch64). Dev : Gérald Kerma (Gandalf). Dépôt :
|
||||
github.com/CyberMind-FR/secubox-deb.
|
||||
Stack : Debian bookworm, kernel 6.x, nftables (PAS iptables), Unbound (Vortex DNS),
|
||||
HAProxy + mitmproxy (WAF), Suricata + CrowdSec, FastAPI/Uvicorn (sockets unix par
|
||||
module), LXC (pas Docker pour les apps), WireGuard, SQLite par défaut.
|
||||
Palette cyberpunk/hermétique : cosmos #0a0a0f, gold #c9a84c, cinnabar #e63946,
|
||||
matrix #00ff41, void #6e40c9, cyan #00d4ff. Polices Cinzel / IM Fell / JetBrains Mono.
|
||||
|
||||
# À LIRE EN PREMIER (sources de vérité)
|
||||
1. CLAUDE.md + .claude/CLAUDE.md — règles impératives.
|
||||
2. .claude/WIP.md — travail en cours + « Next Up ».
|
||||
3. .claude/HISTORY.md — historique daté (commence par l'entrée la plus récente).
|
||||
4. .claude/PATTERNS.md, .claude/MODULE-COMPLIANCE.md, .claude/MIGRATION-MAP.md.
|
||||
5. docs/TOOLS.md, scripts/README.md.
|
||||
|
||||
# RÈGLES IMPÉRATIVES (non négociables)
|
||||
- nftables DEFAULT DROP ; jamais iptables ni uci/LuCI.
|
||||
- JAMAIS de waf_bypass : tout le trafic passe par mitmproxy.
|
||||
- Secrets hors code : /etc/secubox/secrets/ chmod 600 ; jamais en clair / en TOML versionné.
|
||||
- En-tête SPDX LicenseRef-CMSD-1.0 sur chaque fichier (vérifié par scripts/license-headers.py --check).
|
||||
- SQLite par défaut (pas MySQL/Postgres sauf exception documentée).
|
||||
- AppArmor enforce + user dédié secubox-<module> par service.
|
||||
- Packaging Architecture:all pour le Python ; debian/compat=13, Standards-Version 4.6.2.
|
||||
override_dh_strip est MORT pour Architecture:all → installer via execute_after_dh_auto_install.
|
||||
- Pas de référence « Claude Code » / outil IA dans les commits/PR.
|
||||
|
||||
# WORKFLOW (multi-agent worktree)
|
||||
- Tout travail non trivial = worktree dédié : bash scripts/agent-worktree.sh start --issue <#>
|
||||
(branche feature/<#>-… ou fix/<#>-… selon le label ; master réservé au housekeeping).
|
||||
- Cycle : issue GitHub → worktree → commits « (ref #<#>) » → PR « Closes #<#> » →
|
||||
merge → agent-worktree.sh clean <#>. Ne jamais fermer une issue automatiquement.
|
||||
- Build .deb : cd packages/<pkg> && dpkg-buildpackage -us -uc -b -d (le -d ok pour arch:all).
|
||||
|
||||
# DÉPLOIEMENT LIVE (board « gk2 »)
|
||||
- SSH : root@192.168.1.200 (LAN) ou root@10.98.0.1 (tunnel wg-admin) ; clé en place.
|
||||
- Portail toolbox = secubox-toolbox.service (host, uvicorn secubox_toolbox.app:app
|
||||
sur 0.0.0.0:8088). HAProxy : kbin.gk2.secubox.in → backend toolbox_landing → 10.99.0.1:8088.
|
||||
- R3 = 4 workers host-native secubox-toolbox-mitm-wg-worker@{1..4}.service
|
||||
(mitmdump 10.99.1.1:8081-8084) chargeant les addons depuis
|
||||
/usr/lib/secubox/toolbox/mitmproxy_addons/ (liste dans sbin/secubox-toolbox-mitm-wg-launch).
|
||||
- Recette deploy : build → scp .deb → dpkg -i --force-confold --force-confdef →
|
||||
TOUJOURS vérifier portail actif ET curl -sk https://kbin.gk2.secubox.in/ == 200
|
||||
(un upgrade SIGTERM le portail ; le postinst le relance depuis 2.6.29, mais vérifie).
|
||||
Changement d'addon → redémarrer les 4 workers SÉQUENTIELLEMENT (RAM limitée).
|
||||
Ne PAS faire de restart de masse secubox-* (~100+ daemons).
|
||||
|
||||
# ARCHITECTURE TOOLBOX (module le plus actif)
|
||||
packages/secubox-toolbox/ : FastAPI (secubox_toolbox/api.py, app.py), addons
|
||||
mitmproxy (mitmproxy_addons/), filtres modulaires (secubox_toolbox/filters.py →
|
||||
/etc/secubox/toolbox/filters.json, togglés via /admin/filters/ui). Store social :
|
||||
SQLite /var/lib/secubox/toolbox/toolbox.db (social_edges/nodes/links/host_meta/
|
||||
antibot/opgrade + threat_intel). Cartographie : www/toolbox/social.js (vues donut /
|
||||
domaines-nuggets / œil), index.html (WebUI 5 onglets). Addons : inject_banner,
|
||||
protective_mode, ad_ghost, media_cache, media_stats, social_graph, dpi, cookies,
|
||||
avatar, ja4, utiq_defense, cert_pin_detect. Niveaux clients : R0/R1 (sans
|
||||
bannière), R2 (captif), R3 (tunnel WG 10.99.1.0/24), R4 (prévu).
|
||||
|
||||
# ÉTAT ACTUEL (2026-06-14 — RAFRAÎCHIR depuis HISTORY avant réutilisation)
|
||||
secubox-toolbox 2.6.36 déployé live, kbin sain. Live : protective spoofer,
|
||||
filtres modulaires + ad-ghoster (collapse), media cache (opt-in), autolearn
|
||||
trackers, DPI media donut, cartographie donut + nuggets domaine (IPs cachées) +
|
||||
favicons, bannière guirlande + pin partagé, panneau protection webext,
|
||||
/ca/fingerprint R3, fix postinst (kbin 503), detect_antibot deployment-vs-challenge.
|
||||
Clients : APK Android v0.3.0 (zero-tap), webext v0.1.4. Fix : sync photos
|
||||
iPhone↔Nextcloud (files_antivirus off + limites PHP).
|
||||
|
||||
# TRAVAIL OUVERT
|
||||
#592 secubox-webmail-hub : inbox unifié Gmail (OAuth2) + Gandi + OVH ssl0, toutes
|
||||
les sous-boîtes/alias en une page. Design filé, BLOQUÉ : besoin d'un client OAuth
|
||||
Google (client_id/secret/redirect) + nom de vhost + décision read-only. Phase 1
|
||||
IMAP (Gandi/OVH) peut démarrer sans OAuth.
|
||||
|
||||
# TES PREMIÈRES TÂCHES
|
||||
1. ANALYSE (sans rien modifier) : lis .claude/* + CLAUDE.md, puis produis une
|
||||
synthèse structurée — architecture, état des modules (✅/🔄/⬜ via
|
||||
MIGRATION-MAP.md), dette technique, risques sécurité, écarts CSPN, backlog
|
||||
priorisé. Cite chemin:ligne.
|
||||
2. Propose un plan pour l'item « Next Up » (ou #592), conforme au workflow worktree
|
||||
+ aux règles, AVANT d'écrire du code.
|
||||
3. Toute action sur le board live : décris-la et demande confirmation si difficile
|
||||
à annuler ou exposée.
|
||||
|
||||
Commence par : « J'ai lu CLAUDE.md, .claude/WIP.md et HISTORY.md. Voici ma synthèse… »
|
||||
```
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# FAQ — kbin & le mode Tor anonymisé
|
||||
|
||||
> kbin (`kbin.gk2.secubox.in`) = le portail public de la **ToolBoX** SecuBox, premier
|
||||
> outil du couteau suisse cyber CyberMind. Cette FAQ couvre le surf protégé et le futur
|
||||
> **mode Tor quick-switch** ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).
|
||||
|
||||
---
|
||||
|
||||
### Qu'est-ce que kbin exactement ?
|
||||
|
||||
Le portail public de `secubox-toolbox`. On rejoint l'AP libre de la cabine, on consent,
|
||||
et tout le trafic traverse le pipeline de forge MITM SecuBox : inspection chiffrée,
|
||||
nettoyage pub/tracker, bandeau de transparence, safe browsing. Voir
|
||||
[Kbin-Toolbox](wiki/Kbin-Toolbox.md).
|
||||
|
||||
### kbin voit-il tout mon trafic ? C'est pas dangereux ?
|
||||
|
||||
C'est **consenti et éphémère**. La MAC est hashée avec un sel rotatif 24 h, aucune valeur
|
||||
de cookie brute n'est persistée, aucun mapping session ↔ identité réelle ne survit au TTL.
|
||||
Trois niveaux d'opt-in : R0 (bypass complet), R1 (analyse passive, recommandé), R2/R3
|
||||
(TLS-break + bandeau). Sans consentement, **pas** de déchiffrement.
|
||||
|
||||
### « Performance transparente », ça veut dire quoi ?
|
||||
|
||||
On ne déchiffre que ce qu'on modifie. Les flux pur-asset (vidéo, images CDN) sont
|
||||
*splicés* dès le ClientHello TLS (`tls_splice`, #649) — les workers ne forgent/déchiffrent
|
||||
pas ce qui n'a aucune valeur L7. Débit ligne, latence quasi nulle.
|
||||
|
||||
### C'est quoi « l'injection de poison et de smog » ?
|
||||
|
||||
Le trafic ad-tech et tracker n'est pas seulement bloqué : il est **empoisonné**. Anti-Track
|
||||
v2 (#633) renvoie des pseudo-réponses, neutralise les scripts CDN préchargés, et au niveau
|
||||
réseau fait de l'IP-drop + DNS-refuse. Le profil publicitaire ressort pollué, pas vide —
|
||||
indistinguable d'un vrai blocage côté tracker.
|
||||
|
||||
### Le bandeau anti-adware, il bloque quoi ?
|
||||
|
||||
Une bannière de transparence injectée dans la page : nombre de trackers vus/bloqués,
|
||||
acteurs reconnus cross-site. Elle est immune au CSP et SPA-aware (#636/#639, webext #655).
|
||||
C'est l'affichage ; le blocage réel vient des blocklists Vortex DNS + blacklist nft.
|
||||
|
||||
---
|
||||
|
||||
## Mode Tor (plan #683)
|
||||
|
||||
### Le mode Tor, ça fait quoi ?
|
||||
|
||||
Un interrupteur 🧅 sur kbin : un tap → ton surf ressort **par le réseau Tor** au lieu du
|
||||
WAN de la box. IP de sortie anonyme, identité réseau masquée — du « pseudo-network
|
||||
surfing ».
|
||||
|
||||
### Est-ce que kbin arrête de m'inspecter/protéger en mode Tor ?
|
||||
|
||||
Non. Tor se place **après** le cœur de forge MITM, sur le transport upstream (dialer
|
||||
SOCKS5). Tu gardes le poison/smog, le bandeau et le safe browsing ; **seules l'IP de sortie
|
||||
et l'identité réseau changent**.
|
||||
|
||||
### Et si Tor tombe, ça repasse en clair ?
|
||||
|
||||
**Jamais.** Le design est **fail-closed** : si Tor n'est pas disponible, le trafic est
|
||||
coupé, pas renvoyé en clearnet. L'anonymat est un invariant, pas un best-effort.
|
||||
|
||||
### Y a-t-il des fuites DNS ?
|
||||
|
||||
Non. Quand le mode Tor est actif, la résolution passe **par Tor**, pas par l'Unbound local.
|
||||
|
||||
### C'est la même chose que `secubox-exposure` ?
|
||||
|
||||
Non, direction opposée. `secubox-exposure` publie des **services cachés** Tor (entrant —
|
||||
exposer un service interne). kbin Tor endpoint fait sortir ton **surf** par Tor (sortant).
|
||||
Le contrôle Tor (bootstrap, NEWNYM/nouvelle identité) est réutilisé entre les deux.
|
||||
|
||||
### Comment je change d'IP de sortie ?
|
||||
|
||||
Bouton « nouvelle identité » (NEWNYM) → nouveau circuit Tor → nouvelle IP de sortie, à la
|
||||
volée, sans reconnecter.
|
||||
|
||||
### C'est activé par défaut ?
|
||||
|
||||
Non. **Opt-in par client** (scopé WG-hash), **défaut OFF**, respecte ton niveau de
|
||||
consentement R. Chaque bascule on/off est journalisée (audit-log CSPN immuable).
|
||||
|
||||
---
|
||||
|
||||
## Voir aussi
|
||||
|
||||
- [Kbin-Toolbox](wiki/Kbin-Toolbox.md) — la page use-case complète
|
||||
- [Spec mode Tor](superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md)
|
||||
- [Anti-Track](wiki/Anti-Track.md) — bloque/empoisonne/anonymise (couche DNS/IP)
|
||||
|
||||
---
|
||||
|
||||
*CyberMind — Gérald Kerma · LicenseRef-CMSD-1.0*
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||
# SecuBox-Deb — CSPN Test Matrix (draft)
|
||||
|
||||
Maps the ANSSI **CSPN** evaluation themes + the project's stated security
|
||||
functions (CLAUDE.md §"Contraintes ANSSI CSPN") to **concrete, mostly
|
||||
automatable tests**. Target home for the automated rows: `tests/cspn/`
|
||||
(pytest, gated in CI). Each row is an *acceptance check* with a command/
|
||||
assertion and the evidence artifact an evaluator would expect.
|
||||
|
||||
**Legend** — Type: `A`=automated (pytest/CI), `M`=manual/pentest, `D`=doc/spec.
|
||||
Status: ⬜ todo · 🔄 partial · ✅ covered.
|
||||
|
||||
> Scope note: the **cible de sécurité** (security target) must be written
|
||||
> first (TOE boundary, assumptions, threats, security functions). This
|
||||
> matrix is the *robustness + conformity* test plan that hangs off it.
|
||||
|
||||
---
|
||||
|
||||
## 0. Security target & conformity (D)
|
||||
| ID | Requirement | Type | Method / artifact | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| ST-01 | Cible de sécurité rédigée (TOE, hypothèses, menaces, FS) | D | `docs/cspn/cible-securite.md` reviewed | doc complete + signed | ⬜ |
|
||||
| ST-02 | TOE boundary & versions pinned | D | version manifest (pkg list + hashes) per release | matches APT repo | ⬜ |
|
||||
| ST-03 | Conformity: spec ↔ impl traceability | D | each FS → code path + test ID | 100% FS mapped | ⬜ |
|
||||
|
||||
## 1. Cryptography — TLS / keys / RNG
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| CRY-01 | TLS 1.3 min; TLS ≤1.1 refused (HAProxy frontends) | A | `openssl s_client -tls1_1 -connect <vhost>:443` → handshake fail; `-tls1_3` → ok | 1.0/1.1/1.2-weak refused | ⬜ |
|
||||
| CRY-02 | Strong cipher suites only (no RC4/3DES/CBC-legacy) | A | `nmap --script ssl-enum-ciphers` / testssl.sh grade ≥ A | A grade, no weak | ⬜ |
|
||||
| CRY-03 | HSTS + secure headers on exposed vhosts | A | `curl -sI` → `Strict-Transport-Security`, `X-Content-Type-Options` | present | ⬜ |
|
||||
| CRY-04 | Private keys 0600, owner-restricted, not world-readable | A | `stat -c %a` on `/etc/secubox/**/key.pem`, ACME keys | 600, non-root svc owner | 🔄 |
|
||||
| CRY-05 | CA / mitm keys never in VCS or logs | A | `git grep -nE 'BEGIN (RSA |EC )?PRIVATE KEY'` == empty; journald scrub | no hits | ⬜ |
|
||||
| CRY-06 | RNG source = kernel CSPRNG for tokens/keys | A | code audit: `secrets`/`os.urandom`, no `random` for security | no `random.` in sec paths | 🔄 |
|
||||
| CRY-07 | mitm R3 CA fingerprint published & verifiable | A | `/ca/fingerprint?ca=wg` == cert on disk (sha256) | match (D5:E4:3A…) | ✅ |
|
||||
|
||||
## 2. Authentication & session
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| AUT-01 | All API endpoints require JWT (`Depends(require_jwt)`) | A | enumerate FastAPI routes; assert auth dep except allowlist | 100% gated | 🔄 |
|
||||
| AUT-02 | Unauthenticated request → 401, no data leak | A | `curl` each `/api/v1/*` sans token | 401, empty body | ⬜ |
|
||||
| AUT-03 | JWT signature verified; tampered/expired rejected | A | forge/expire token → 401 | rejected | ⬜ |
|
||||
| AUT-04 | Social/report tokens = HMAC, TTL-bound, salt-rotated | A | expired/forged `/social/{token}` → 403; salt rotates daily | rejected + rotation | 🔄 |
|
||||
| AUT-05 | No default/hardcoded credentials | A | grep configs + first-boot generates per-device secrets | none | 🔄 |
|
||||
| AUT-06 | Brute-force handled at the WAF layer (per project doctrine) | M | rate-limit probe via HAProxy/CrowdSec | throttled/banned | 🔄 |
|
||||
| AUT-07 | ZKP auth (GK-HAM-2025) NIZK soundness, G rotation 24h PFS | M+A | protocol test vectors + rotation timer check | proofs verify, rotates | ⬜ |
|
||||
|
||||
## 3. Access control / privilege separation
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| ACL-01 | Each daemon runs as `secubox-<module>` (not root) | A | `systemctl show -p User` over all `secubox-*` units | non-root each | 🔄 |
|
||||
| ACL-02 | AppArmor profile present + **enforce** per service | A | `aa-status` lists each profile in enforce | all enforce | ⬜ |
|
||||
| ACL-03 | systemd hardening (ProtectSystem, NoNewPrivileges, etc.) | A | `systemd-analyze security secubox-*` score | exposure ≤ medium | ⬜ |
|
||||
| ACL-04 | Filesystem perms: `/etc/secubox/secrets` 0600, parents traversable but not writable | A | `stat` perms + traversal test as svc user | 0600 secrets, 0755 parents | 🔄 |
|
||||
| ACL-05 | No unintended setuid/world-writable shipped | A | `find / -perm -4000 / -perm -0002` in image | known allowlist only | ⬜ |
|
||||
|
||||
## 4. Network filtering / attack surface
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| NET-01 | nftables policy DEFAULT DROP (input/forward) | A | `nft list chain inet filter input` → `policy drop` | drop | ✅ (verify) |
|
||||
| NET-02 | Only declared ports open; no stray listeners | A | `ss -tlnp` ∩ documented port map | exact match | 🔄 |
|
||||
| NET-03 | WAN-side SSH closed (key-only + source-restricted) | A | sshd `PasswordAuthentication no`; nft SSH-guard drops non-LAN/tunnel | enforced | ✅ |
|
||||
| NET-04 | No IPv6 leak past the v4 firewall guards | A | nft inet covers v6; `ss` v6 listeners reviewed | covered | ⬜ |
|
||||
| NET-05 | nft rules persist across reboot + survive pkg upgrade | A | reboot/upgrade → drop-ins reload; ruleset intact | persists | 🔄 |
|
||||
| NET-06 | DNS = Unbound only; DoH/DoT exfil detected/blocked (opt-in) | A | resolve via Unbound; DoH probe flagged | controlled | 🔄 |
|
||||
|
||||
## 5. WAF / traffic inspection integrity (no bypass)
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| WAF-01 | No `waf_bypass` anywhere; all vhosts → mitm inspector | A | grep HAProxy cfg; each backend = mitmproxy_inspector (or documented exception) | no bypass | 🔄 |
|
||||
| WAF-02 | mitm CA only trusted on consenting (R2/R3) clients | A | non-consenting client not MITM'd | scoped | ✅ |
|
||||
| WAF-03 | Banner/transparency shown to inspected clients (CSPN R2 req) | A | inspected HTML carries the banner guard | present | ✅ |
|
||||
| WAF-04 | Active interference (spoof/ghost) is opt-in + logged + reversible | A | filters default-safe; every action → audit.log; toggle off restores | conforms | ✅ |
|
||||
| WAF-05 | mitm fail-open never serves attacker-controlled content | M | malformed upstream / addon exception → flow unbroken, no inject error | safe | 🔄 |
|
||||
|
||||
## 6. Logging & audit (immutability)
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| LOG-01 | Security decisions (ban/unban/spoof/escalate/rule-change) logged to `/var/log/secubox/audit.log` | A | trigger each → grep audit line | logged | 🔄 |
|
||||
| LOG-02 | Timestamps RFC 3339 / ISO-8601 with TZ | A | regex each audit line | conforms | 🔄 |
|
||||
| LOG-03 | Append-only / rotation without truncate (immutability) | A | `chattr +a` or rotate-copy-truncate disabled; tamper test | no in-place edit | ⬜ |
|
||||
| LOG-04 | Logs free of secrets/PII (mac→hash, no tokens) | A | grep audit/journal for token/cookie/key patterns | none | 🔄 |
|
||||
| LOG-05 | Audit survives service crash + reboot | A | crash mid-write → log consistent | intact | ⬜ |
|
||||
|
||||
## 7. Configuration management & rollback (4R / double-buffer)
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| CFG-01 | Sensitive config change = shadow→validate→atomic swap | A | `secubox-params swap` flow; partial write never live | atomic | ⬜ |
|
||||
| CFG-02 | 4R rollback restores prior state (R1..R4 snapshots) | A | mutate → `rollback --target R1` → state == pre | restored | ⬜ |
|
||||
| CFG-03 | Validation rejects bad config before swap (4R: Read→Write→Validate→Rollback/Commit) | A | inject invalid → swap refused, live unchanged | refused | ⬜ |
|
||||
| CFG-04 | Config swap audit-logged + (ZKP-gated where required) | A | swap → audit line | logged | ⬜ |
|
||||
|
||||
## 8. Update mechanism
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| UPD-01 | APT repo GPG-signed; unsigned/altered pkg refused | A | tamper a .deb → `apt` refuses | refused | 🔄 |
|
||||
| UPD-02 | Upgrade preserves runtime state + restarts services (no outage) | A | upgrade → portal up, kbin 200, nft intact (regression of #581) | no downtime | ✅ |
|
||||
| UPD-03 | Downgrade / rollback path defined | D+A | pinned prior version installs cleanly | works | ⬜ |
|
||||
| UPD-04 | Reproducible build / provenance | A | CI build hashes recorded per release | recorded | 🔄 |
|
||||
|
||||
## 9. Data protection at rest
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| DAT-01 | Secrets only under `/etc/secubox/secrets` 0600, svc-owned | A | inventory + `stat` | conforms | 🔄 |
|
||||
| DAT-02 | No secrets in code / TOML / git history | A | `git log -p` + `git grep` secret patterns | none | 🔄 |
|
||||
| DAT-03 | SQLite stores hashed identifiers (mac_hash, cookie_id_hash), not raw PII | A | schema + sample-row audit | hashed | 🔄 |
|
||||
| DAT-04 | Data retention enforced (social 7d, logs rotation) | A | retention timers prune | enforced | 🔄 |
|
||||
|
||||
## 10. Resilience / fail-safe
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| RES-01 | Service crash → auto-recovery (watchdog), portal probe | A | kill portal → restored + kbin 200 | recovers | ⬜ |
|
||||
| RES-02 | RAM-pressure: no OOM cascade under load (Armada budget) | M+A | load test; per-service MemoryMax; no thundering-herd | stable | 🔄 |
|
||||
| RES-03 | Fail-secure: filter/addon error must not open the WAF or break pages | A | inject addon exception → default-drop / fail-open page-safe | secure | 🔄 |
|
||||
|
||||
## 11. Hardening / vulnerability management
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| HRD-01 | No known-vuln Python deps | A | `pip-audit` / safety in CI | 0 high/critical | ⬜ |
|
||||
| HRD-02 | No known-vuln OS packages in the image | A | `debsecan`/trivy on the image | 0 high/critical | ⬜ |
|
||||
| HRD-03 | Attack-surface minimal: unused services disabled | A | enabled-units ∩ required set | minimal | 🔄 |
|
||||
| HRD-04 | SAST clean on the codebase | A | `bandit` (py) in CI | no high | ⬜ |
|
||||
| HRD-05 | Pentest of the exposed surface (kbin, HAProxy, R3) | M | grey-box assessment report | no critical | ⬜ |
|
||||
|
||||
## 12. Conformity glue (CI)
|
||||
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||
|----|-------------|------|-------------------|------|----|
|
||||
| CI-01 | `tests/cspn/` runs in CI, gates merge | A | workflow job red on fail | gating | ⬜ |
|
||||
| CI-02 | Coverage ≥80% on security-critical modules | A | coverage report | ≥80% | ⬜ |
|
||||
| CI-03 | `compliance-lint` (AppArmor/user/secrets/no-bypass/SPDX) per PR | A | linter job | clean | 🔄 (SPDX only) |
|
||||
|
||||
---
|
||||
|
||||
## How to operationalise
|
||||
1. Write the **cible de sécurité** (ST-01) — everything else traces to it.
|
||||
2. Scaffold `tests/cspn/` (pytest), one module per theme above
|
||||
(`test_crypto.py`, `test_authz.py`, `test_firewall.py`, `test_audit.py`,
|
||||
`test_rollback.py`, …). Each `XXX-NN` ID = one test function id.
|
||||
3. Add a CI job (CI-01) running it against a built image / a staging board.
|
||||
4. Add `compliance-lint` (CI-03) for the static rows (perms, AppArmor,
|
||||
no-bypass, SPDX, no-secrets).
|
||||
5. Burn down ⬜→✅; the ✅ rows above are already verifiable today.
|
||||
|
||||
Priority order (highest CSPN risk first): **§6 audit immutability**, **§7
|
||||
4R rollback**, **§3 AppArmor enforce + privilege**, **§1 TLS**, **§12 CI
|
||||
gate/coverage** — these are the criteria most likely to fail an assessment
|
||||
today given the current ~9% test coverage.
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |