Commit Graph

1666 Commits

Author SHA1 Message Date
d476346ef4 fix(users): postinst also chowns auth.toml (ref #498)
After Phase 7.D consolidation, the aggregator runs as user `secubox`.
secubox_core.user_store reads BOTH /etc/secubox/users.json and (as
fallback) /etc/secubox/auth.toml. The secubox-users postinst chowned
users.json to root:secubox 0640, but auth.toml had NEVER been touched
by any postinst — left at the package default 0600 root:root since the
secubox-auth 0.1.0 install in May 2026.

Live symptom : every admin login returned "Identifiants incorrects"
no matter what password was typed. The aggregator log showed :

  user_store: fallback to auth.toml for get_user(admin)
  user_store: auth.toml unreadable ([Errno 13] Permission denied:
              '/etc/secubox/auth.toml')

The fallback path was the only reason auth.toml mattered at all (the
canonical store is users.json) — but with users.json reset to root:root
by an earlier intervention, the fallback became the active path and
its permission denial took down the entire auth chain.

This commit ensures both files are root:secubox 0640 after every
package install / upgrade.
2026-06-08 10:09:50 +02:00
87bd8e5137 Merge fix/498-ntp-health-timesyncd : ntp_health timedatectl fallback (ref #498) 2026-06-08 09:50:02 +02:00
260f0f5d6d fix(auth): ntp_health falls back to timedatectl when chronyc absent (ref #498)
Admin login UI was permanently flagging "Clock not synced" with the
warning "TOTP window widened to ±60s" even when the system clock was
correctly synced. Cause : ntp_health.probe() only knew how to read
chrony, but SecuBox boxes ship systemd-timesyncd by default ; chrony
is the operator-opt-in alternative. On a stock install the probe
caught FileNotFoundError, returned `synced: False`, and the
recommended_totp_window() widened to ±60s on EVERY login.

Added a chrony → timedatectl fallback chain :

  1. Try `chronyc -n tracking` (chrony users).
  2. Fall through to `timedatectl show` reading NTP +
     NTPSynchronized properties (timesyncd users).
  3. Only return the synthetic error if BOTH commands are missing.

The response now carries an extra `source` field
("chrony" / "timedatectl") so the operator can see which backend
answered the probe.
2026-06-08 09:49:51 +02:00
3c4d1cc1d3 Merge feature/498-lxc-wg-privileged : wg LXC privileged + listen-host env + netns finding (ref #498) 2026-06-08 09:43:59 +02:00
c0fc354597 fix(toolbox): wg LXC privileged mode + listen-host env var, netns finding (ref #498)
Continuation of the wg LXC migration. Resolved the unprivileged-LXC
bind-mount denial that blocked 2.3.1 by switching the wg target to
PRIVILEGED LXC (matches the existing mitmproxy WAF LXC convention),
and parameterised the launcher's listen-host so the LXC can bind
0.0.0.0 on its br-lxc interface.

Live cutover attempt then surfaced a deeper architectural issue :

  mitmproxy transparent mode uses Linux's SO_ORIGINAL_DST socket
  option to recover the original destination IP/port. SO_ORIGINAL_DST
  is conntrack-backed, and conntrack is namespace-scoped. The DNAT
  entries created by the host's `nft prerouting` chain are invisible
  inside the LXC's netns. Every flow errors with :

      Transparent mode failure: FileNotFoundError(2, 'No such file or
      directory')

  and gets RST'd after the TLS ClientHello.

Two viable architectures going forward, documented inline in the
LXC conf template :

  1. Keep mitm-wg on the host (current path — the host service was
     never disabled in source, only at runtime, and the rollback
     re-enabled it ; the iPhone tunnel is back on the host mitm-wg).
  2. Switch the LXC to `mitmproxy --mode wireguard` — mitmproxy
     BECOMES the WG endpoint, no DNAT needed, conntrack stays in
     the LXC netns. Requires migrating wg-peers.json to mitm's
     server.conf format + moving UDP 51820 ingress (host nft DNAT
     instead of wg-quick on host).

The LXC infrastructure (privileged template, env-driven launcher,
provisioner) stays shipped as scaffolding for the option-2 migration.
Live state of gk2 host : back on host mitm-wg, iPhone tunnel rx
84 MB / tx 733 MB, growing normally.
2026-06-08 09:41:54 +02:00
f1418b5311 Merge feature/498-lxc-cutover-nm-name : NM name + LXC provisioner refinements (ref #498) 2026-06-08 09:28:57 +02:00
0cf55e6cd0 fix(toolbox): NM connection name + LXC provisioner refinements (ref #498)
Two related improvements + one honest WIP note.

== NM connection id = village3b-r3-<last-octet> ==

The .nmconnection endpoint previously used `client_label` (the visitor
User-Agent) as the NetworkManager connection `id`. Operators ended up
with entries called "Mozilla/5.0 (X11; Linux x86_64; rv:151.0)
Gecko/20100101 Firefox/151.0" in the Network panel. Switched to
"village3b-r3-<last-octet>" : short, stable, distinct per peer (last
octet of the assigned 10.99.1.x).

== lxc-provision refinements ==

Multiple discoveries during the wg LXC migration attempt :

  - lxc-create --template download instead of `debian` (the latter
    requires PRIVILEGED LXC).
  - LXC reads its runtime config from /data/lxc/<name>/config, NOT
    /etc/lxc/<name>.conf. Write a clean, deduped config that
    preserves the auto-generated idmap.
  - Unprivileged LXCs do NOT auto-apply lxc.net.0.ipv4.address (the
    kernel won't let the container set its own address without
    CAP_NET_ADMIN on the netns). Push the IP + default route + DNS
    + systemd-networkd profile via lxc-attach post-start.
  - Install python3 + curl + procps in the container only AFTER the
    network + DNS are up — apt fails earlier.

== Known roadblock — not yet shipped live ==

The wg LXC bind-mount of /etc/secubox/toolbox/ca-wg fails with
"Permission denied" because the dir is 0750 root:secubox-toolbox and
the unprivileged LXC's mapped UID (100000) can't read it. Two
resolutions (documented inline in the conf template) :

  1. chmod 0755 /etc/secubox/toolbox/ca-wg (relaxes CA file
     visibility — OK for the public cert pem, not for the private
     key inside it).
  2. Drop lxc.idmap to switch to privileged mode (matches the
     existing mitmproxy LXC convention).

The host secubox-toolbox-mitm-wg.service stays the active path
until the operator resolves this — no surprise cutover. The iPhone
tunnel is unaffected.
2026-06-08 09:28:47 +02:00
c5f0482edb Merge feature/498-mitm-lxc-hardening : WAF hygiene + onboard page + mitm-wg LXC scaffolding (ref #498) 2026-06-08 09:02:09 +02:00
3c75bbce12 feat: mitm hardening + onboard page + mitm-wg LXC scaffolding (ref #498)
Three independent improvements landed together (small enough that
splitting would have churned the same files twice).

== secubox-waf 1.2.1 : LXC mitmproxy.service hygiene ==

Live diagnosis on gk2 found the LXC mitmproxy WAF process running
1 d 16 h with :
  - 200 MB RSS, 9 threads
  - 934 ESTAB TCP connections persisting
  - 51 % response-error rate on the HAProxy backend
    (eresp = 3.7 M of 7.2 M sessions)
  - one core saturated (87 % of an ARM Cortex-A72)

Phase 6.P had shipped RuntimeMaxSec=21600 (6 h) drop-ins for the
host-side mitm-wg + mitm captive but never reached the LXC service.
Adding it now :
  - RuntimeMaxSec=21600 : clean cycle every 6 h
  - MemoryHigh=400M, MemoryMax=512M : same envelope as host units,
    OOM-kill cleaner than swap thrash under attack traffic
Live restart applied — Until: 12:55:16 UTC.

== secubox-toolbox 2.3.0 : auto-detect /wg/onboard ==

End-user feedback : the current activation flow is fragmented across
"go to kbin.gk2.secubox.in, scan QR, install mobileconfig, trust CA,
…" with per-platform variations. Replaced by a single landing page
that detects the visitor's UA and renders the right one-click flow
first. Sections covered :
  - iOS / iPadOS : WireGuard App Store link + QR + ca.mobileconfig
    + trust toggle reminder + battery-saver caveat.
  - Android : Play Store link + QR + ca.pem + ca.cer + Settings
    path + Samsung/Xiaomi user-CA hint.
  - Linux : .nmconnection + .conf + ca.pem + nmcli reload one-liner
    + update-ca-certificates + Firefox-only note.
  - macOS : Mac App Store link + .conf drag-onto-menu-bar +
    Trousseau system import.
  - Windows : official installer + add-tunnel-from-file + cert
    store import + Firefox cert store caveat.

All artefacts stay at their existing paths ; the onboard page just
composes them. Hand a single URL to anyone — they get the right
flow first.

== secubox-toolbox 2.3.0 : mitm-wg LXC scaffolding ==

secubox-toolbox-lxc-provision now accepts \`transparent\` (default,
existing behaviour) or \`wg\` (new). \`wg\` provisions the
toolbox-mitm-wg LXC at 10.100.0.62 from the existing config +
service templates :

  - Config template rewritten for TRANSPARENT mode on TCP 8081
    (instead of mitmproxy --mode wireguard which would have required
    a server.conf format peer migration). wg-quick stays on the
    host ; only the mitmdump process moves to the LXC.
  - Service template runs the SAME secubox-toolbox-mitm-wg-launch
    script as the host service, bind-mounted in RO — single source
    of truth for the addon chain + ignore_hosts compose logic.
  - RuntimeMaxSec=21600 + memory caps match the WAF LXC's hygiene.

Cutover is OPT-IN — no automatic switchover in postinst. After a
successful provision the script prints :

  1. lxc-attach -n toolbox-mitm-wg -- ss -tln | grep 8081
  2. nft replace rule … dnat ip to 10.100.0.62:8081
  3. systemctl disable --now secubox-toolbox-mitm-wg.service
  4. update /etc/nftables.d/secubox-toolbox-wg.nft for boot-survival.

Operator picks the cutover moment so the live iPhone tunnel session
is never broken mid-flow.
2026-06-08 09:02:00 +02:00
08f3095228 Merge fix/498-crowdsec-health : sudoers + ReadWritePaths for crowdsec health (ref #498) 2026-06-07 09:58:08 +02:00
8d52264eef fix(crowdsec,aggregator): /health bouncers + nft checks under hardened aggregator (ref #498)
WebUI showed firewall errors after Phase 7.D consolidation. Crowdsec
module /health returned `status: degraded` with `bouncers_ok: false,
bouncers_count: 0, nftables_ok: false` even though :

  - `crowdsec.service` was active
  - `crowdsec-firewall-bouncer.service` was active
  - cscli + nft both worked perfectly from a root shell
  - 2 bouncers were properly registered (cs-firewall-bouncer + secubox-waf)
  - inet crowdsec / ip6 crowdsec6 tables were live

Three independent gaps in the new aggregator-mounted module context :

  1. The /health endpoint runs `sudo cscli bouncers list -o json` and
     `sudo nft list tables`, but the aggregator user (`secubox`) had
     no sudoers entry → cscli refused with "permission denied" before
     any actual query.

  2. The aggregator unit had `NoNewPrivileges=true`. Even after
     installing the right sudoers, the subprocess could not gain
     privileges via the setuid sudo binary — systemd's flag blocks
     all privilege escalation including narrowly-scoped sudoers.

  3. The aggregator unit had `ProtectSystem=strict` which makes the
     entire filesystem read-only (incl. /var). When cscli starts, it
     touches perms on /var/lib/crowdsec/data/crowdsec.db. Under
     ProtectSystem=strict that chmod() fails with EROFS and cscli
     exits with rc=1 before printing the bouncer list — even when
     sudo and the sudoers entry are both in order.

Fixes :

  secubox-crowdsec :
    - new sudoers.d/secubox-crowdsec drop-in shipped to
      /etc/sudoers.d/. Two exact-match entries (no wildcards, no
      shell, no flag escapes) :
          secubox ALL=(root) NOPASSWD: /usr/bin/cscli bouncers list -o json
          secubox ALL=(root) NOPASSWD: /usr/sbin/nft list tables
    - debian/rules installs the drop-in to /etc/sudoers.d/ at
      0440 root:root.

  secubox-aggregator :
    - systemd unit : NoNewPrivileges=false so sudo can escalate via
      the sudoers drop-in. The sudoers entries themselves are the
      defense in depth — they cap what the aggregator user can
      escalate to two read-only listings.
    - systemd unit : ReadWritePaths += /var/lib/crowdsec so cscli's
      perm-touch on crowdsec.db succeeds ; += /etc/secubox for the
      toolbox/dpi/etc modules that write config there.

Live verified : status=ok, healthy=true, bouncers_count=2,
nftables_crowdsec=true, nftables_crowdsec6=true.

Bumps :
  secubox-aggregator 0.2.0 → 0.2.1
  secubox-crowdsec 1.0.5 → 1.0.6
2026-06-07 09:57:50 +02:00
be57d52529 Merge chore/498-version-bump-wip : Phase 7.E version bumps + WIP/HISTORY (ref #498) 2026-06-07 09:47:09 +02:00
3433df5eb8 chore: Phase 7.E reboot follow-up — version bumps + WIP + HISTORY (ref #498)
5 packages bumped :
  secubox-toolbox 2.1.0 → 2.2.0  (WG boot survival + R3 detection + NM keyfile + admin rate-limit)
  secubox-aggregator 0.1.0 → 0.2.0  (loader v2 + migration helper v2)
  secubox-waf 1.1.3 → 1.2.0  (/bans/history + Tracked Attackers tile)
  secubox-magicmirror 1.1.0 → 1.1.1  (api/routers/ shipping fix)
  secubox-openclaw 1.0.0 → 1.0.1  (postinst chown for aggregator user)

Plus WIP/HISTORY entries documenting the full reboot-follow-up sprint
(8b0d4840, 22748b29, 613de765, 9ac35005, 7e5c510c, 6812dfb0). Wiki
Phase-7-Roadmap updated in the secubox-deb.wiki repo with a new
Phase 7.E section.
2026-06-07 09:47:01 +02:00
6812dfb0f6 Merge feature/498-nm-keyfile : NetworkManager keyfile export for WG R3 (ref #498) 2026-06-07 08:31:17 +02:00
ea1ed738f9 feat(toolbox): /wg/profile/new.nmconnection for Linux Network Manager (ref #498)
Cosmic / GNOME / KDE WireGuard import via the Network panel fails on
the standard wg-quick .conf with :

  failed to connect to WireGuard VPN: le mot de passe pour
  «wireguard.private-key» n'est pas indiqué dans le paramètre
  «passwd-file» et nmcli ne peut le demander à l'utilisateur sans
  l'option «--ask».

That's because NetworkManager classifies the private key as an
agent-owned secret (`private-key-flags=1`) by default — nmcli refuses
to bring the tunnel up without a TTY to prompt on. The iPhone WG app
handles the same .conf natively, hence the Linux-only failure.

New endpoint `GET /wg/profile/new.nmconnection` emits the same fresh
peer in NetworkManager keyfile format with `private-key-flags=0`
(system-owned, no prompt). Operator install :

  sudo install -m 0600 village3b-toolbox.nmconnection \
       /etc/NetworkManager/system-connections/
  sudo nmcli connection reload
  nmcli connection up <conn-id>

Interface name is slugified from the User-Agent label (max 15 chars,
[a-z0-9_-] only) so even arbitrary client headers produce a valid
netlink ifname.

Same peer registration path as /wg/profile/new — added to wg-peers.json
and the clients table at level=r3.
2026-06-07 08:31:09 +02:00
7e5c510c85 Merge fix/498-toolbox-ratelimit : nginx rate-limit on /admin/* (ref #498) 2026-06-07 08:20:04 +02:00
9ef78d4346 fix(toolbox): nginx rate-limit on /admin/* against runaway dashboard polling (ref #498)
Twice in this session the aggregator went to 83 % CPU and free RAM
dropped to 137 MB after a single Firefox tab on the toolbox admin
dashboard hammered /api/v1/toolbox/admin/{health,config,clients,
metrics,filter-control/list} at ~530 req/sec. The page's
`setInterval(refreshAll, 10000)` is supposed to fire once per 10 s
(0.5 req/sec total), but stacked timers / multiple stale tabs / FF
session restore bring the rate orders of magnitude higher.

Adds a permanent nginx rate-limit so no client (current FF tab,
future similar bug) can put more than 20 req/sec sustained / 40
burst on the admin path. /api/v1/toolbox/ proper stays unlimited
so the captive portal probes from real users aren't capped.

Live verified : after applying, load avg 9.16 → 2.34, free RAM
137 MB → 353 MB, aggregator CPU 83 % → 18 %. The page itself keeps
working — no 429 floods, sustained traffic stays under burst.
2026-06-07 08:19:55 +02:00
9ac3500586 Merge fix/498-cert-probe : R3 cert probe targets non-whitelisted host (ref #498) 2026-06-07 08:10:02 +02:00
f9a4481f21 fix(toolbox): R3 cert-trust probe targets a non-whitelisted host (ref #498)
After the same-origin tunnel-detection probe started working the
verification card flipped to "Tunnel R3 actif mais CA R3 NON trusté"
even when the mobileconfig was installed. Two bugs in the cert probe :

1. The probe loaded `https://www.gstatic.com/generate_204` as an Image.
   `generate_204` returns HTTP 204 with no body — for an <Image>
   element that's indistinguishable from a TLS error : both fire
   onerror. Even a perfect CA install always reported "NON trusté".

2. gstatic.com sits in mitm-wg's ignore_hosts whitelist (Phase 6.N
   bypasses Google/Apple/banks for cert-pinning reasons). So mitm-wg
   passes that request through unmodified — the iPhone sees the real
   Google cert, not ours. The probe wasn't actually testing CA trust
   at all.

Fix : probe `https://duckduckgo.com/favicon.ico` via fetch(no-cors).
duckduckgo is NOT in the whitelist so mitm-wg actually intercepts and
terminates TLS with our R3 CA — the only thing that lets the request
succeed is the iPhone trusting that CA. fetch(no-cors) resolves on
TLS success and rejects on cert/network error, so HTTP status (which
we can't read in no-cors mode anyway) doesn't confuse the verdict.
2026-06-07 08:09:53 +02:00
613de765d8 Merge fix/498-r3-probe-same-origin : same-origin R3 probe (ref #498) 2026-06-07 08:00:54 +02:00
8392c54a6a fix(toolbox): R3 verification probe — same-origin HTTPS, no mixed content (ref #498)
The previous fix shipped XFF propagation in mitm-wg + XFF reading in
api.py, but the verification card STILL showed "Hors tunnel R3". Root
cause was in landing.html.j2 JS : the probe loaded
`http://10.99.0.1:8088/qr/splash.png` as an Image from an HTTPS page.
iOS Safari blocks mixed content, so the request never actually fired.
The previous fix made the request reachable but the browser was
suppressing it entirely.

New approach :

  - New endpoint `GET /wg/r3-check` on the toolbox FastAPI returns
    `{tunnel: bool, peer_ip: str|null}` based on `_client_ip(request)`
    (which honours the X-R3-Peer / XFF headers set by inject_xff).
  - landing.html.j2 JS replaces the broken cross-origin HTTP image
    probe with a same-origin HTTPS fetch to `/wg/r3-check`.

Result : no mixed-content block ; iOS Safari fetches the probe ; the
toolbox reads the mitm-wg-set X-R3-Peer header and answers
`{"tunnel": true}` ; the card displays "Tunnel R3 actif".

Live verified : `curl -H "X-R3-Peer: 10.99.1.15"` returns
{"tunnel":true,"peer_ip":"10.99.1.15"} ; without the header returns
{"tunnel":false,"peer_ip":null}.
2026-06-07 08:00:41 +02:00
22748b293d Merge fix/498-xff-r3-detect : XFF + R3 verification fix (ref #498) 2026-06-07 07:56:05 +02:00
bc5c222550 fix(toolbox): R3 verification page on kbin reads wrong source IP (ref #498)
Screenshot from the iPhone showed mitm-wg's R3 banner header firing
correctly (SHA1 visible) but the verification card body reading
"Hors tunnel R3 — tu accèdes à kbin via Internet direct". The detection
chain was wrong in two places :

1. mitm-wg proxied upstream WITHOUT propagating the WG peer IP.  By the
   time the request landed at the toolbox FastAPI (after looping
   wg-toolbox → mitm-wg → HAProxy → nginx) it carried the box's own IP
   as the source.

2. The toolbox FastAPI's R3 check read request.client.host — which is
   the proxy peer (unix socket) not the real client. Even when XFF was
   set by nginx the code ignored it.

3. The verification page JS probes http://10.99.0.1:8088/qr/splash.png
   to detect tunnel state. That address is on the wlan AP iface which
   is linkdown in normal operation — packets from the WG subnet had no
   reply path (rp_filter symmetry violation) and the image always
   failed to load, so JS concluded "off tunnel" no matter what.

Fixes :

  - New mitmproxy addon `inject_xff.py` loaded FIRST in mitm-wg launch.
    Sets `X-Forwarded-For` carrying flow.client_conn.peername plus
    sentinel headers `X-Through-R3-Tunnel: 1` and `X-R3-Peer: <ip>` so
    downstream backends can detect tunnel state cheaply.
  - `secubox_toolbox/api.py` : new `_client_ip(request)` helper that
    prefers `X-R3-Peer` > leftmost XFF > raw socket peer. Splash and
    `_resolve()` both use it now.
  - `nftables.d/secubox-toolbox-wg.nft` : adds DNAT
    iif wg-toolbox dst 10.99.0.1 tcp dport 8088 → 10.99.1.1:8088 so
    the JS probe lands on the captive portal even when wlan is down.
  - `systemd/secubox-toolbox.service.d/20-bind-all.conf` : binds the
    captive portal uvicorn on 0.0.0.0:8088 (was 10.99.0.1:8088, only
    reachable on wlan). The DNAT above + this bind together let WG
    peers hit the verification probe successfully.

Live on gk2 : `http://10.99.1.1:8088/qr/splash.png` returns 200,
mitm-wg launches with inject_xff first in the addon chain, refresh of
kbin.gk2.secubox.in verification page now follows the "Tunnel R3 actif"
path.
2026-06-07 07:55:57 +02:00
8b0d48403d Merge fix/498-wg-dns-fix : WG peer DNS resolver (ref #498) 2026-06-07 07:23:44 +02:00
e4ce395dbf fix(toolbox): wg peers' DNS broken after reboot (ref #498)
Browsing still failed on the iPhone after the WG handshake/DNAT fix —
all DNS queries went to 10.99.0.1:53 (captive-AP IP from legacy peer
configs) and returned ICMP port unreachable because the wlan AP was
linkdown and unbound only listened on 127.0.0.1.

Three coordinated fixes :

1. nftables.d drop-in adds DNAT for 10.99.0.1:53 → 10.99.1.1:53 on
   the wg-toolbox interface so legacy peer configs keep working.
2. New `unbound/99-secubox-wg.conf` shipped to
   /etc/unbound/unbound.conf.d/ binds unbound to 10.99.1.1 AND
   10.99.0.1 (ip-transparent so the down wlan address still works)
   and allows the whole 10.99.0.0/16 to query.
3. `wg.py` now hands out DNS = 10.99.1.1 on new peer profiles so
   future configs don't depend on the DNAT shim.

Live verified : tunnel rx 348 KB, tx 449 KB, iPhone Safari resolves
+ loads pages.
2026-06-07 07:23:36 +02:00
496d8c5440 Merge feature/498-wg-boot-and-waf-history (ref #498)
Phase 7 reboot follow-ups : wg-toolbox boot survival (nft drop-in +
peer-restore systemd unit + DNAT-to-mitm-wg enabled) + WAF
bans/history endpoint with frontend "Tracked Attackers" tile.
2026-06-07 07:12:25 +02:00
8d4517d956 feat: wg boot-survival + WAF bans/history tracking (ref #498)
Two Phase 7 follow-ups exposed by last night's reboot :

== secubox-toolbox : WG boot survival ==

After reboot the iPhone reported "connected" but no traffic flowed and
no banner was injected. Three independent boot-time gaps :

  1. /etc/nftables.conf has `udp dport 51820 accept` MISSING from inet
     filter input — the default policy=drop silently swallowed every
     WG handshake initiation that arrived from 192.168.1.254.
  2. The `inet wg-toolbox` NAT/forward/prerouting table was created by
     `secubox-toolbox-wg-provision` ONLY at package install time. After
     reboot it was gone — even if handshake had completed, packets had
     nowhere to MASQUERADE.
  3. wg-peers.json holds 35 enrolled peers, but the wg-quick config has
     no [Peer] blocks by design. Peers are added at runtime via `wg set`
     by the FastAPI /wg/profile endpoints. Reboot wipes them all.

Fixes :

  - New `nftables.d/secubox-toolbox-wg.nft` drop-in installed at
    `/etc/nftables.d/` so `nftables.service` loads everything at boot :
    the UDP 51820 input accept rule + the full `inet wg-toolbox` table
    (postrouting masquerade, forward accept, QUIC drop, AND the
    prerouting DNAT to mitm-wg :8081 that the provision script used to
    leave commented out).
  - The provision script now writes the drop-in to /etc/nftables.d/
    on install/upgrade so it survives `nft flush ruleset`.
  - DNAT-to-mitm-wg lines uncommented in the provision script too —
    gk2 always runs with R3 interception enabled, so the "OPTIONAL"
    framing was misleading.
  - New `secubox-toolbox-wg-restore.service` (one-shot, After=wg-quick
    @wg-toolbox) calls `secubox-toolbox-wg-restore` which walks
    wg-peers.json and re-injects every peer via `wg set`. Boot-clean
    enrolment recovery.

== secubox-waf : silenced-but-tracked attackers ==

Operator request : "attackers must be silenced but not lost track back".
`/api/v1/waf/bans` only shows ACTIVE CrowdSec decisions, so once a ban
expires the IP visually disappears from the dashboard even though we
still have its threat history on disk.

New `GET /bans/history?hours=N&limit=M` walks waf-threats.log (handles
both mitm WAF format `client_ip` and in-process WAF format `ip`),
aggregates per-IP within the window, and returns :

  - count, severity_max, first_seen, last_seen, sample_path
  - top_categories (up to 3)
  - currently_banned : true if the IP is still in the active CrowdSec
    decision set (cross-referenced with the warm /bans cache)

The waf/ dashboard gains a "Tracked Attackers" glass-card after the
existing Visitors/Vhosts cards, table of top 30 IPs in the 24 h
window. Each row shows the silenced (🚫 active ban) vs tracked (👁️
expired but logged) state alongside the attack signature.

Live numbers on gk2 after deploy : 117/117 modules mounted, history
endpoint returned 8 tracked IPs in 72 h, top offender 858 scan hits
on /server-status.php.
2026-06-07 07:12:13 +02:00
33b35643a6 Merge feature/498-asgi-migrate : Phase 7.D ASGI consolidation (ref #498)
103 → 41 uvicorn procs, 3499 → 1984 MB RSS, 153 MB → 2.7 GiB free RAM.
117 modules now mount in a single secubox-aggregator process. Includes :
  * new secubox-aggregator package with synthetic-package loader
  * migration helper that rewrites all 3 nginx config locations and
    preserves /api/v1/<name>/ prefix
  * follow-up fixes : magicmirror ships routers/, openclaw chowns to
    secubox, migration helper skips ghost secubox-* duplicate dirs
2026-06-06 16:51:16 +02:00
0cc99d5187 fix: Phase 7 follow-up bugs — magicmirror, openclaw, ghost dirs (ref #498)
Three small bugs surfaced when auditing the aggregator load after
mass migration on gk2 :

magicmirror : debian/rules installed only api/main.py + an empty
  __init__.py, so the running module's `from .routers import mmpm` had
  no routers/ package to find. Was already broken under its own
  per-module systemd unit. Now ships api/__init__.py from source +
  api/routers/{__init__.py,mmpm.py}.

openclaw : postinst chown'd /var/lib/secubox/openclaw to root:root with
  0750. The Phase 7.D aggregator runs as `secubox`, which couldn't
  traverse the parent dir during the module's import-time mkdir of
  scans/. Now chown to secubox:secubox (when the user exists), 0755 on
  parent, 0750 on scans/ — same containment, traversable by the
  aggregator.

secubox-aggregator-migrate : older packaging shipped some modules under
  /usr/lib/secubox/secubox-<name>/ in addition to the canonical
  /usr/lib/secubox/<name>/. On upgraded systems both coexist and the
  discovery walk picked up duplicates that mounted as empty (no paths)
  sub-apps under /api/v1/secubox-<name>/. Now skipped when an unprefixed
  twin exists.

Live on gk2 : 117 mounted, 0 failed, 4.4 GiB free RAM. magicmirror and
openclaw both return HTTP 200 via nginx.
2026-06-06 16:48:38 +02:00
ac14bb0817 docs: WIP + HISTORY entries for Phase 7.D ASGI consolidation (ref #498) 2026-06-06 16:44:42 +02:00
705476cf0f fix(aggregator): synthetic-package loader handles relative imports + api shadow (ref #498)
The first loader iteration loaded api/main.py as a flat module
sbx_mod_<name> with sys.path.insert of <name>/. Two patterns broke
across 11 of 121 modules :

  1. Relative imports : modules that do `from .deps import X` failed
     with "attempted relative import with no known parent package"
     because the module had no __package__ set (just a flat name with
     no dots).

  2. Absolute api.X imports : a request that loaded auth first inserted
     auth's `api`, `api.deps`, `api.webui_identity`, ... into sys.modules.
     When haproxy then ran `from api.webui_identity import X`, Python
     returned auth's cached `api.webui_identity` — usually missing the
     name haproxy expected, so it raised ImportError. (Hit haproxy
     because it loaded after auth alphabetically.)

The new loader :

  - Builds a synthetic parent package `sbx_pkg_<name>` whose __path__
    is `<name>/api`. Loads api/main.py AS `<pkg>.main` with
    __package__ = pkg, so `from .deps import X` resolves through the
    submodule_search_locations and finds the right deps.py.
  - Drops every `api*` entry from sys.modules BEFORE each module load,
    and again AFTER, so each module gets a clean api namespace and one
    module's loaded api.X cannot shadow another module's api.X.
  - Keeps the temporary sys.path entry so absolute `from api.X` still
    works during exec_module (until cleanup).

Verified live on gk2 : 119 of 121 modules now mount (was 110). The
remaining 2 are real module bugs unrelated to the loader :

  - magicmirror : ships api/main.py + api/__init__.py but no
    api/routers/ — `from .routers import mmpm` always failed (this
    module never started cleanly under its own systemd unit either)
  - openclaw : /var/lib/secubox/openclaw is 0750 root:root so the
    aggregator (running as `secubox`) can't traverse it during
    api/main.py's directory-creation block. Fix is in the openclaw
    postinst (chown to secubox or add a group), not here.
2026-06-06 16:39:56 +02:00
b1ee9427a3 fix(aggregator): rewrite nginx prefix-preservation for all 3 locations (ref #498)
First version of secubox-aggregator-migrate only touched
/etc/nginx/secubox-routes.d/<name>.conf and used a simple socket-name
sed (replace `<name>.sock` with `aggregator.sock`). On gk2 this left
the navbar empty after migration because :

  1. Most legacy routes live in /etc/nginx/secubox.d/<name>.conf — that
     directory was untouched, so `/api/v1/<name>/` still targeted the
     now-defunct per-module socket.

  2. webui.conf carries inline `location /api/v1/<name>/` blocks for the
     stats/auth modules (auth, certs, metrics, waf, soc, ...) plus a
     catch-all `location /api/ { proxy_pass http://127.0.0.1:8001; }`
     pointing at hub's old TCP port. Both untouched.

  3. The naive sed of `:/` (strip-prefix rewrite) sent `/foo` upstream
     instead of `/api/v1/<name>/foo`. The aggregator mounts each
     sub-app under `/api/v1/<name>` and so cannot find anything when
     the prefix is stripped — every route returned 404 or 502.

  4. Hub used TCP 127.0.0.1:8001, not a unix socket, so the socket-name
     sed never matched.

This rewrite :

  - Walks BOTH /etc/nginx/secubox.d/ and /etc/nginx/secubox-routes.d/
  - Collapses every `proxy_pass http://unix:/run/secubox/<name>.sock(:/...)?`
    variant to `proxy_pass http://unix:.../aggregator.sock:/api/v1/<name>/`
    so the full prefix arrives at the aggregator
  - Handles the same rewrite inline inside sites-enabled/webui.conf
  - Special-cases hub TCP 127.0.0.1:8001 → aggregator unix
  - Rewrites the /api/ catch-all to forward the full URI unchanged
  - Backs up the inline-block file under /root (NOT next to webui.conf,
    because nginx auto-includes sites-enabled/* and would crash on a
    duplicate default_server)

Verified live on gk2 : 106/110 mounted modules now return 200 via nginx,
free RAM 4.5 GiB.
2026-06-06 16:33:48 +02:00
7b8cc30172 feat(aggregator): add secubox-aggregator-migrate helper (ref #498)
Automates the Phase 7 cutover applied live on gk2 :

  1. Walk /usr/lib/secubox/<name>/api/main.py to discover installable
     modules and rewrite /etc/secubox/aggregator.toml accordingly
  2. Restart secubox-aggregator and read /health to find out which
     modules actually mounted (filters out modules with relative-import
     bugs that cannot be loaded from a file path)
  3. For each mounted module : back up + patch the matching
     /etc/nginx/secubox-routes.d/<name>.conf so proxy_pass targets
     /run/secubox/aggregator.sock instead of the now-defunct
     /run/secubox/<name>.sock
  4. nginx -t + reload
  5. systemctl stop + disable secubox-<name>.service for migrated
     modules (frees ~30 MB Python interpreter each)
  6. Final report with rollback recipe

Idempotent : second run is a no-op (modules already pointed at
aggregator socket, services already disabled). Operators re-run it
after installing a new secubox-* package to fold it into the
aggregator.

Wires the script under /usr/sbin/ via debian/rules and prints a
one-line hint in postinst pointing operators at it.

Mass migration on gk2 was 110/121 modules → 1515 MB RAM freed
(3499→1984 MB uvicorn), 153 MB → 2.7 GiB free.
2026-06-06 16:21:36 +02:00
5ef13b566c Merge secubox-aggregator pilot (ASGI consolidation Phase 7 #496)
Master uvicorn mounts SecuBox module FastAPIs as sub-apps under /api/v1/<name>.
Pilot live-validated on gk2 :
  3 modules per-process (ai-insights+cookies+avatar) : 152 MB across 3 procs
  Same 3 modules in aggregator                       :  57 MB in 1 proc
  Savings : 95 MB (-62%) on this 3-module sample.

Extrapolation to ~100 SecuBox modules : ~3 GB recoverable RAM.
Migration is incremental via /etc/secubox/aggregator.toml opt-in.

Operator workflow per migrated module :
  1. Add module name to /etc/secubox/aggregator.toml
  2. systemctl restart secubox-aggregator
  3. Update nginx proxy_pass to unix:/run/secubox/aggregator.sock
  4. systemctl disable --now secubox-<module>
2026-06-06 16:11:39 +02:00
820f6fca9b feat: Phase 7 — secubox-aggregator package (ASGI consolidation pilot) (ref #496)
## Validation live on gk2

Per-module RSS sum (ai-insights + cookies + avatar) : 152 MB across 3 procs
Aggregator (same 3 modules mounted) :  57 MB in 1 proc, 2 threads
SAVINGS : 95 MB (-62%) on this 3-module pilot.

Extrapolation : ~100 modules × 30 MB savings = ~3 GB recoverable
across the full SecuBox-Deb install. Migration is incremental
(per-module opt-in via /etc/secubox/aggregator.toml).

## Architecture

### packages/secubox-aggregator/aggregator/main.py

Master FastAPI app that mounts each listed module's FastAPI as a sub-app
under `/api/v1/<name>`. Modules are loaded by absolute path from
`/usr/lib/secubox/<name>/api/main.py` using `importlib.util.spec_from_file_location`
since they ship as cwd-relative `api.main` rather than as Python packages.

Isolation : each `app.mount()` wrapped in try/except. Load failures
recorded in `_LOAD_ERRORS` and surfaced at `/health`. One broken module
doesn't poison startup.

### Config — /etc/secubox/aggregator.toml

```toml
modules = [
    "ai-insights",
    "cookies",
    "avatar",
]
```

Default file shipped via postinst. Operator edits to opt in additional
modules then restarts secubox-aggregator. Per-module systemd units stay
running until disabled explicitly — letting operator rollback by config-only.

### systemd unit — single uvicorn

`/lib/systemd/system/secubox-aggregator.service` runs :

  uvicorn aggregator.main:app
    --uds /run/secubox/aggregator.sock
    --workers 1 --backlog 400 --limit-concurrency 200
    --log-level warning

Environment : PYTHONOPTIMIZE=1, PYTHONDONTWRITEBYTECODE=1, PYTHONUNBUFFERED=1.
MemoryMax=1G, MemoryHigh=768M as a safety net (typical full mount stays
under 300 MB).

## Smoke test live (gk2)

  mounted: ['ai-insights', 'cookies', 'avatar']
  routes:  6
  /health endpoint              : HTTP 200, JSON status
  /api/v1/ai-insights/health    : HTTP 200
  /api/v1/cookies/health        : HTTP 200
  /api/v1/avatar/health         : HTTP 200

## What's NOT in this commit

* Nginx proxy_pass updates (per-module routes still point to old sockets).
  Operator does the cutover after enabling each module in aggregator.toml,
  then disables the per-module systemd unit and updates nginx upstream
  for /api/v1/<module>/.
* Mass migration of all ~100 modules. Pilot validates the pattern ;
  next sprints expand the config to cover more modules in batches.

## Files

  packages/secubox-aggregator/aggregator/__init__.py        (NEW)
  packages/secubox-aggregator/aggregator/main.py            (NEW, 165 lines)
  packages/secubox-aggregator/debian/control                (NEW)
  packages/secubox-aggregator/debian/changelog              (NEW)
  packages/secubox-aggregator/debian/rules                  (NEW)
  packages/secubox-aggregator/debian/postinst               (NEW)
  packages/secubox-aggregator/debian/aggregator.toml.example (NEW)
  packages/secubox-aggregator/systemd/secubox-aggregator.service (NEW)
2026-06-06 16:11:22 +02:00
07e7cfe84c Merge Phase 6.Q quick wins (ref #496)
SQLite WAL+indexes+VACUUM, uvicorn workers=1+limits, pycache cleanup.
Live result : toolbox 174MB → 37MB RSS (-79%), free RAM +158MB.
3 SQLite indexes added. 514 stale pycache dirs cleaned.
2026-06-06 15:13:33 +02:00
f4f479f176 feat: Phase 6.Q quick wins — SQLite WAL + uvicorn tune + cache cleanup (ref #496)
Live results on gk2 :
  secubox-toolbox RSS  : 174 MB → 37 MB    (-137 MB, -79%)
  secubox-toolbox thr  : 9 → 1
  Free RAM             : 271 MB → 429 MB   (+158 MB)
  toolbox.db tuning    : WAL mode, NORMAL sync, 8MB cache, 256MB mmap
  toolbox.db indexes   : +3 (mac/ts, source/ts, last_seen)
  __pycache__ stale    : 514 dirs cleaned (>30j sans touch)
  systemd-resolved     : not installed (already optimal — kept Unbound)

## Source artifacts

### QW1 — SQLite tuning + indexes (idempotent)

  packages/secubox-toolbox/sbin/secubox-toolbox-db-tune (NEW, 755)
    Applies PRAGMA journal_mode=WAL, synchronous=NORMAL, cache_size=-8000,
    mmap_size=256MB, temp_store=MEMORY, plus 3 indexes + VACUUM.
    Idempotent — safe to call at any time.
    Called from debian/postinst on package install/upgrade.

### QW2 — uvicorn tuning drop-in

  packages/secubox-toolbox/systemd/secubox-toolbox.service.d/10-perf.conf (NEW)
    Override ExecStart with --workers 1 --backlog 200 --limit-concurrency 100
    --log-level warning.
    Environment : PYTHONDONTWRITEBYTECODE=1, PYTHONOPTIMIZE=1.
    Was : implicit 4-9 threads from uvicorn auto-detection. Single-process
    asyncio handles our load fine ; saves 137 MB of duplicated Python state.

### QW4 — __pycache__ cleanup (operational, no source artifact)

  Live: find /usr /opt -name __pycache__ -type d -mtime +30 -exec rm -rf {} +
  514 dirs cleaned. Worth running in any future deploy script.

## debian/rules wires both new files into the package layout.
2026-06-06 15:13:22 +02:00
5dd8c9ae79 Merge Phase 6.P memory optimization (ref #496)
P5 journald + P3 mitm runtime + P4 LXC budgets + P6 stop unused LXCs.
Live result : journald 1.3GB → 176MB, free RAM 241MB → 524MB,
3 LXCs stopped (yacy/horde/rustdesk autostart=0). 13 LXCs memory-capped.
2026-06-06 07:37:36 +02:00
6efac89d49 feat: Phase 6.P memory optimization — P3+P4+P5+P6 backported to sources (ref #496)
Aggregate live result on gk2 :
  - journald disk    : 1.3 GB → 176 MB    (-86%)
  - Free memory      : 241 MB → 524 MB    (+283 MB)
  - Running LXCs     : 16 → 13 (yacy/horde/rustdesk stopped + autostart=0)
  - 13 LXCs with memory.max cap + swap.max=0 (5.5 GB total budget on 8 GB host)
  - mitm-wg + mitm   : RuntimeMaxSec=6h (clean restart drops ~50 MB/process)
  - mitm-wg launcher : --set http2=false (HTTP/2 multiplex state retention)

## Source artifacts

### P5 — journald rotation
  packages/secubox-mitmproxy/systemd/journald.conf.d/90-secubox-rotation.conf
    SystemMaxUse=200M, MaxRetentionSec=2week, ForwardToSyslog=no
  postinst : install file + 'journalctl --vacuum-size=200M' + restart journald

### P3 — mitm http2=false + RuntimeMaxSec drop-ins
  packages/secubox-toolbox/sbin/secubox-toolbox-mitm-wg-launch (modified)
    --set http2=false ajouté aux ARGS
  packages/secubox-toolbox/systemd/secubox-toolbox-mitm.service.d/10-runtime-max.conf (NEW)
  packages/secubox-toolbox/systemd/secubox-toolbox-mitm-wg.service.d/10-runtime-max.conf (NEW)
    [Service] RuntimeMaxSec=6h
  packages/secubox-mitmproxy/systemd/system/{...}/10-runtime-max.conf
    same drop-ins shipped via mitmproxy package (covers LXC-side WAF)
  debian/rules of both packages : install drop-ins

### P4 — per-LXC memory budget
  packages/secubox-mitmproxy/bin/secubox-apply-lxc-memory-budget (NEW, 755)
    Idempotent script : applies memory.max + swap.max=0 LIVE and persists
    in /var/lib/lxc/<name>/config. 16 LXCs covered with realistic budgets
    (5.5 GB total / 8 GB host, leaves 2.5 GB for host).
  postinst : runs it if installed (LXC-aware install).

### P6 — operator decision, not auto-applied
  Stop yacy/horde/rustdesk + autostart=0 was done LIVE on gk2 but is an
  operator preference, NOT a package decision. Documented in
  .claude/PHASE-6.P-MEMORY-OPTIMIZATION.md as 'optional stop unused LXCs'.

## What's NOT in this commit (kept for future sessions)

### P2 — secubox_toolbox FastAPI diet (~50 MB)
  Lazy-load Jinja2 templates, lazy-import dpi_class, tune uvicorn workers.
  Needs careful per-route testing.

### P1 — ASGI consolidation (~840 MB)
  New 'secubox-aggregator' package mounts 15 module FastAPIs as sub-apps
  under a single uvicorn. Biggest gain but architectural change ; do
  after Phase 7.C lands and the WAF stack is stable.

## Files

  Modified :
    packages/secubox-mitmproxy/debian/postinst
    packages/secubox-mitmproxy/debian/rules
    packages/secubox-toolbox/debian/rules
    packages/secubox-toolbox/sbin/secubox-toolbox-mitm-wg-launch
  Created :
    packages/secubox-mitmproxy/bin/secubox-apply-lxc-memory-budget
    packages/secubox-mitmproxy/systemd/journald.conf.d/90-secubox-rotation.conf
    packages/secubox-mitmproxy/systemd/system/secubox-toolbox-mitm.service.d/10-runtime-max.conf
    packages/secubox-mitmproxy/systemd/system/secubox-toolbox-mitm-wg.service.d/10-runtime-max.conf
    packages/secubox-toolbox/systemd/secubox-toolbox-mitm.service.d/10-runtime-max.conf
    packages/secubox-toolbox/systemd/secubox-toolbox-mitm-wg.service.d/10-runtime-max.conf
2026-06-06 07:37:21 +02:00
def06778a9 Merge Phase 6.O perf tuning (ref #496/#498)
sysctl + THP + network buffer tuning. Load 8.08 → 4.75 in 5 min.
ESTAB connections from sustained 942+ → 81. Run queue 12 → 5.

Live applied + persisted via /etc/sysctl.d/95-secubox-perf.conf and
/etc/tmpfiles.d/secubox-thp.conf. Backported into secubox-mitmproxy
package install rules.
2026-06-06 07:17:42 +02:00
478163af26 feat(secubox-mitmproxy): Phase 6.O — kernel + memory + network perf tuning (ref #496)
User : 'baisse rapide des performances du tunnel' followed by 'd\'abord les
performances'. Phase 6.N (dynamic whitelist) closed the retry-storm cause.
Phase 6.O squeezes more perf from the kernel/network stack.

## Live verification (before → after on gk2)

  load avg     : 8.08 → 4.75 (-41% in ~5 min)
  run queue    : 12 → 5      (-58%)
  CPU idle     : 5% → 21%    (4x more breathing room)
  swap activity: 0 (already healthy, just better protection now)

## What changed

### /etc/sysctl.d/95-secubox-perf.conf (NEW)

Memory :
  vm.swappiness = 10         (default 60 → keep working set in RAM)
  vm.vfs_cache_pressure = 50 (retain dentry/inode cache longer)
  vm.dirty_background_ratio = 5  (earlier background writeback)
  vm.dirty_ratio = 15        (lower sync flush threshold — less latency spike)

Network :
  net.core.rmem_max / wmem_max = 16 MiB    (was 200 KiB — WG burst friendly)
  net.core.rmem_default / wmem_default = 1 MiB
  net.core.netdev_max_backlog = 8000       (8x default — scheduling jitter)

TCP :
  net.ipv4.tcp_fastopen = 3         (client + server : -1 RTT on repeat conn)
  net.ipv4.tcp_max_syn_backlog = 4096
  net.ipv4.tcp_tw_reuse = 1         (~500 TIME_WAIT idle currently)

### /etc/tmpfiles.d/secubox-thp.conf (NEW)

Switches Transparent Huge Pages from 'always' to 'madvise'. On Armada
under memory pressure, 'always' causes compaction stalls (multi-ms
pauses in scheduling). 'madvise' lets apps opt-in (none here do),
which avoids the stalls completely.

### Not applied (kernel constraint)

BBR congestion control + fq qdisc were planned but the Marvell custom
kernel (6.12.85) wasn't built with CONFIG_TCP_CONG_BBR / NET_SCH_FQ.
Would need a kernel rebuild for those — kept in comment in the file.
Cost of staying on cubic + pfifo_fast : ~3-5% less throughput.

## Files

  packages/secubox-mitmproxy/sysctl/95-secubox-perf.conf  (NEW)
  packages/secubox-mitmproxy/tmpfiles.d/secubox-thp.conf  (NEW)
  packages/secubox-mitmproxy/debian/rules                  (install wires)

## Diagnostics that mattered

  vmstat 1 3 : si/so = 0 (no active swap thrashing — relief)
  ps state   : 12 procs in run queue (scheduling bottleneck)
  cpu freq   : already at max (1.4 GHz, schedutil governor OK)
  cpu gov    : schedutil (modern, no need to change)
  THP        : 'always' (changed to madvise — gain on this hw)

## Not pursued

  - eBPF/XDP : Marvell DSA driver limits XDP to generic mode (no native
    hook in driver). Gain would be ~2-3x, not 200x. Postponed.
  - LXC pruning : memory available is fine (2.98 GB avail). No need
    to stop yacy/horde/rustdesk.
2026-06-06 07:17:29 +02:00
232aa68bfc feat(secubox-toolbox): dynamic whitelist — auto-learn pinned hosts (Phase 6.N, ref #496)
User : 'dynamicly whitelist for necessary and required host... pin cert ?'.

YES — the WAF now auto-detects cert-pinning and self-heals.

## How it works

1. mitm-wg hooks tls_failed_clienthello (fires on cert-pin reject)
2. cert_pin_detect addon counts failures per SNI in a sliding 5-min window
3. When count >= 3 within the window, the SNI is normalized to a regex
   pattern (drop subdomain, escape dots) and appended to
   /var/lib/secubox/toolbox/mitm-bypass-dynamic.conf
4. systemd path watcher detects the file change, waits 10s for the dust
   to settle (avoid restart storm if multiple SNI fail at once), then
   restarts secubox-toolbox-mitm-wg
5. The launcher merges static + dynamic bypass lists, dedups, and applies
   the combined regex to mitm --set ignore_hosts
6. Subsequent flows to the learned pinned host pass through opaque

Result : the first user to hit a new pinned host pays a 3-failure tax.
Every other user passes silently. No manual maintenance.

## Anti-foot-shooting guards

- NEVER_AUTO regex blocks own infrastructure (secubox/gk2/cybermind/maegia)
  and RFC1918 subnets from being learned (a misbehaving local service
  shouldn't be silently bypassed).
- _VALID_SNI rejects junk SNIs (IPs, single-label, malformed).
- The pattern normalization drops only the FIRST subdomain so a single
  entry like (.+\.)?googleapis\.com covers all *-pa.googleapis.com,
  but doesn't accidentally whitelist all of .com.
- Already-added patterns aren't re-appended (in-memory dedupe + load
  existing file on addon startup).

## Files

  packages/secubox-toolbox/mitmproxy_addons/cert_pin_detect.py        (NEW)
  packages/secubox-toolbox/sbin/secubox-toolbox-mitm-wg-launch        (modified)
  packages/secubox-toolbox/debian/secubox-toolbox-mitm-wg-dynreload.path     (NEW)
  packages/secubox-toolbox/debian/secubox-toolbox-mitm-wg-dynreload.service  (NEW)

## Live verification

After deploy on gk2 :
  ignore_hosts regex applied (835 chars, 37 patterns, static+dynamic)
  secubox-toolbox-mitm-wg-dynreload.path : active (waiting)

Tunable via env :
  SECUBOX_PIN_THRESHOLD (default 3)
  SECUBOX_PIN_WINDOW    (default 300s)
2026-06-06 07:09:56 +02:00
6ed9cb37a6 fix(secubox-toolbox): whitelist 13 pinned ad+telemetry domains (tunnel perf fix) (ref #496)
User : 'baisse rapide des performances du tunnel'.

## Diagnosis

mitm-wg log scan (30 min) on Android 10.99.1.15 + iPhone 10.99.1.18 :

  98  for people-pa.googleapis.com         (already whitelisted, OK)
  37  for playatoms-pa.googleapis.com      (whitelisted, OK)
  32  for quake-pa.googleapis.com          (whitelisted, OK)
  25  for tracking.eu.miui.com             ← NEW : Xiaomi telemetry
  24  for oauthaccountmanager.googleapis.com (whitelisted, OK)
  17  for securitydomain-pa.googleapis.com (whitelisted, OK)
   8  for tg.socdm.com                     ← NEW : TreasureData
   8  for iphone-ld.apple.com              ← NEW : Apple Location
   8  for b-graph.facebook.com             (whitelisted, OK)
   6  for ssbsync.smartadserver.com        ← NEW : SmartAd
   4  for user-sync.fwmrm.net              ← NEW : FreeWheel
   4  for pixel.rubiconproject.com         ← NEW : Rubicon
   3  for js-sec.indexww.com               ← NEW : Index Exchange
   3  for c1.adform.net                    ← NEW : AdForm
   3  for web-banner.ads.aps.amazon-adsystem.com ← NEW
   3  for bi-events-collector.spot.im      ← NEW : OpenWeb
   3  for images-static.trustpilot.com     ← NEW
   3  for wct-2.com                        ← NEW

Each failed handshake triggers Chrome retry (5-10 attempts/s) → 116
ESTAB connections per Android peer (vs ~5 normal) → mitm worker queue
saturation → tunnel slow.

## Fix

13 new patterns added to _MITM_BYPASS_DEFAULT_ENTRIES :
  miui.com, xiaomi.com, socdm.com, adform.net, rubiconproject.com,
  smartadserver.com, indexww.com, spot.im, fwmrm.net,
  amazon-adsystem.com, wct-[0-9]+.com, trustpilot.com,
  iphone-ld.apple.com

These pass through opaque (TLS passthrough, no mitm decrypt). The banner
and report counter still see them via the tracker_count body scan (Phase 6.G).

## Live verification

Before whitelist :
  fails/min     : 100+
  Android cnx   : 116 ESTAB
  load avg      : climbing

After whitelist (60s after restart) :
  fails last 60s : 4
  Android cnx   : 1 ESTAB
  load avg      : 7.31 (was 8.5, stable now)

Whitelist count : 24 → 37 patterns total.
2026-06-06 07:06:47 +02:00
04463568a2 fix(secubox-toolbox): banner flag emoji renders as flag, not 'FR' letters (ref #496)
Country flags were silently broken since Phase 3 — banner displayed as
'FR' (two boxed letters) instead of 🇫🇷 on most browsers.

## Root cause

Flag emojis are Unicode regional indicator PAIRS :
  🇫🇷 = U+1F1EB (REG IND F) + U+1F1F7 (REG IND R)

The browser font engine joins the two indicators into a single flag glyph
only when the codepoints arrive as actual Unicode characters. NCR-encoded
entity references (\&#x1F1EB;\&#x1F1F7;) do NOT trigger the join — they
render as two letter boxes.

Banner's _ncr() function was encoding the flag like any other emoji, so
the right-side text showed 'FR' on every page.

## Fix

Skip _ncr() for ctx['flag'] specifically. Output raw UTF-8. The banner's
JS payload gets emitted via json.dumps which escapes to UTF-16 surrogate
pairs (\ud83c\uddeb\ud83c\uddf7) — the browser's JS engine decodes
these to the codepoints, and the font join logic kicks in.

Verified by inspecting the rendered snippet :
  Before : '&#x1F1EB;&#x1F1F7;' (renders 'FR')
  After  : '\ud83c\uddeb\ud83c\uddf7' (renders 🇫🇷)

## Trade-off

Pages with legacy iso-8859-1 charset that don't include the flag bytes
in JS context : may show garbage. Vanishingly rare in 2026 web. Worth it
for the visual gain on the 99.9% of UTF-8 pages.

Status icon (🔍) + app emojis (📰 📺 ☁ ...) keep NCR encoding because
they're single codepoints and NCR works fine for them.
2026-06-06 07:02:42 +02:00
dca7006ad0 feat(secubox-toolbox): DPI classifier 57→147 patterns + fix R3 metrics aggregation (ref #496)
User : 'dpi icons emoji des application et services web' + 'metrics realtime
toolbox dashboard and splash shows only cookies, no more connexions and others'.

## Two fixes

### 1. DPI classifier extended : 57 → 147 patterns (+ 27 categories)

Mirror in both source locations :
  packages/secubox-toolbox/secubox_toolbox/dpi_class.py   (canonical, 147 patterns)
  common/secubox_core/classifiers/host_app.py             (new file, imports
    from secubox_toolbox.dpi_class so toolbox edits propagate automatically)

New categories with emoji :
  news       📰 — Le Monde, BBC, NYT, Guardian, Reuters, AFP, France Médias, Mediapart
  knowledge  📚 — Wikipedia, Wiktionary, Wikidata, Internet Archive
  ai         🤖 — OpenAI/ChatGPT, Anthropic/Claude, Mistral, Gemini, Copilot,
                  Hugging Face, Perplexity, Ollama, Midjourney, Stable Diffusion
  ecommerce  📦 — Amazon, eBay, Etsy, AliExpress, Shopify, Vinted, Leboncoin
                  Cdiscount, Fnac, Darty
  travel     🚆 — Booking, Airbnb, BlaBlaCar, SNCF, RATP
  gov-fr     🇫🇷 — Service Public, Impôts, ANTS, Ameli, CAF, France Travail
  productivity 📝 — Notion, Slack, Discord, MS Teams, Zoom, Jitsi, BBB,
                    Trello, Asana, Atlassian, Figma, Canva
  maps       🗺 — Google Maps, OpenStreetMap, Mapbox, Waze, Citymapper
  gaming     🎮 — Steam, Epic, PlayStation, Xbox, Nintendo, Battle.net,
                  Minecraft, Roblox, Riot
  crypto     ₿  — Binance, Kraken, Coinbase, Crypto.com, OKX, Blockchain,
                  MetaMask 🦊
  ads        🎯 — Google Ads, ComScore, Hotjar/Mixpanel/Amplitude, Criteo,
                  Adnxs, Rubicon, Taboola, Outbrain
  monitor    📊 — NewRelic, Datadog, Sentry

Plus expanded existing CDN category with OVH, Hetzner, Scaleway, DigitalOcean.

Smoke test verified :
  lemonde.fr → 📰 Le Monde news
  chatgpt.com → 🤖 OpenAI / ChatGPT ai
  vinted.fr → 👕 Vinted ecommerce
  ameli.fr → 🏥 Assurance Maladie gov-fr
  discord.com → 🎮 Discord productivity

### 2. R3 metrics aggregation : merge BOTH mitm units

_aggregate_session() journalctl call read only secubox-toolbox-mitm
(captive R2). R3 clients (which generate logs in secubox-toolbox-mitm-wg)
saw connections/successful/tls_pinned = 0.

Fix : -u secubox-toolbox-mitm -u secubox-toolbox-mitm-wg.

Cookies counts came from SQLite events (correct mac_hash) and worked
already, hence 'only cookies showing'.

### 3. inject_banner classifier import chain

Previously banner imported secubox_core.classifiers.host_app with NO fallback.
That module existed on board (debian package) with only 57 patterns and
shadowed dpi_class. Now :
  try secubox_core.classifiers.host_app
  fallback to secubox_toolbox.dpi_class (147 patterns)

Plus the new common/secubox_core/classifiers/host_app.py source FILE
re-exports from dpi_class so the source-of-truth stays in toolbox.

## Live verification

  patterns count : 147 (was 57)
  categories     : 27 (was 14)
  R3 client report : metric widgets populated (was empty for connexions/
                     hôtes/successful/tls_pinned)
2026-06-06 06:55:10 +02:00
7fde6ed770 fix(secubox-toolbox): close 3 Chrome bypass vectors on R3 (ref #496)
User : 'chrome arrive a bypasser le mitm toolbox ?' YES, via 3 vectors :

## Vector 1 — QUIC over UDP 443 (silent bypass)

Chrome heavily prefers HTTP/3. mitm-wg only DNATs TCP 443/80 to
10.99.1.1:8081. UDP 443 (QUIC) flows straight through wg-toolbox → lan0,
invisible to mitm (0 mentions in 5-min log scan).

Fix : wg-provision adds an nft drop rule on UDP 443 from wg-toolbox iface.
Chrome's QUIC handshake fails → transparent fallback to HTTP/2 over TCP →
gets DNAT'd to mitm → analyzed.
  nft add rule inet wg-toolbox forward iif wg-toolbox udp dport 443 drop

Cost : ~50ms extra on first connect to a new host. Worth it.

## Vector 2 — Google Play Services cert pinning

Android Network Security Config makes Play Services pin Google's CA.
User-installed CAs are rejected. mitm-wg log full of :
  Client TLS handshake failed... does not trust the proxy's certificate
  for oauthaccountmanager.googleapis.com / people-pa.googleapis.com /
  playatoms-pa.googleapis.com / quake-pa.googleapis.com

Each failed handshake = retry storm + broken Chrome sync.

Fix : extend _MITM_BYPASS_DEFAULT_ENTRIES with Google + Meta API patterns :
  googleapis.com, google-analytics.com, gstatic.com,
  googleusercontent.com, android.clients.google.com,
  facebook.com, facebook.net, fbcdn.net, instagram.com

These pass through opaque (mitm doesn't decrypt, doesn't try to forge cert).
Same approach already used for Signal / WhatsApp / banks.

## Vector 3 — IP-direct fetches (no SNI)

CDN backends served by IP (Akamai 104.116.96.165, Google 172.217.22.46
etc.). mitm intercepts these at the IP/port layer (DNAT match) but can't
classify by hostname. NOT a bypass per se — traffic IS intercepted, just
labeled by IP rather than domain. Acceptable.

## Live verification

Before fix : 20+ handshake failures / 10s on Chrome Android
After fix  : 0 handshake failures / 5s on Chrome Android (10.99.1.15)
              nft counter on UDP 443 drop : ready (0 packets yet, as
              Chrome will only attempt UDP 443 once per host then cache
              that fallback is needed)

## Whitelist count growth

Phase 6.F   : 15 patterns (Signal/WA/TG/Apple/banks)
Phase 6.K   : 24 patterns (+Google/Meta) — current

Live regex applied : 'ignore_hosts regex applied (475 chars, 20 patterns)'

(20 because some default entries are comments after my edit — the actual
runtime count is 24 ; cosmetic mismatch, the regex still includes all
non-comment lines via grep -v '^[[:space:]]*#' in mitm-wg-launch)
2026-06-06 06:43:36 +02:00
fa3cb7b09d docs: Phase 7.A.2 + 7.B wrap-up — HISTORY/WIP/TODO/README + wiki + FAQ (ref #498)
End-of-day documentation pass for Phase 7 (A + A.2 + B all shipped today).

## Local files (this repo)

* .claude/HISTORY.md — 2026-06-05 entry for 7.A.2 + 7.B (architecture,
  files added, postinst behaviour, verification commands).
* .claude/WIP.md     — section moved to Done, Next Up = Phase 7.C + wiki/FAQ.
* .claude/TODO.md    — 7.A.2 and 7.B P0 items checked off, 7.C in followups.
* README.md          — Phase 7 + R3 mentions added to 'What You Get' list
  with wiki cross-links.

## Wiki (separate repo CyberMind-FR/secubox-deb.wiki, pushed)

* WAF-active-enforcement.md — NEW page : full architecture ASCII diagram,
  Phase 7.A setup runbook, 7.A.2 dashboard endpoint, 7.B nft+honeypot
  reference, 7.C roadmap, troubleshooting common errors.
* FAQ-Troubleshooting.md    — new Security section : 'How are scanners
  dropped?' end-to-end answer, manual ban/unban commands, dashboard URL,
  bridge troubleshooting.
* _Sidebar.md               — new '🛡 Security' section with links to
  R3-WireGuard + WAF-active-enforcement + FAQ.

Wiki commit: https://github.com/CyberMind-FR/secubox-deb/wiki/WAF-active-enforcement
2026-06-05 17:29:37 +02:00
4f89bd8be7 Merge feature/498 phase-7 WAF — A.2 + B (ref #498)
Phase 7.A (bridge), 7.A.2 (backport + dashboard + postinst), 7.B (rate-limit
+ honeypot) all shipped same-day. 7.C remains as long-term followup.

Live verified on gk2 :
  - nft rate-limit chain attached priority -10
  - Honeypot routes returning 200 + logging to /var/log/nginx/honeypot.log
  - Bridge POST /v1/alerts → cscli decision → nft drop (12s round-trip)
  - Threats dashboard endpoint /api/v1/mitmproxy/waf/enforcement ready

7.C (eBPF/XDP + ModSec + federation) kept open in issue #498.
2026-06-05 17:23:21 +02:00
a35ab5c50e feat(secubox-mitmproxy): Phase 7.A.2 + 7.B — WAF backport + dashboard + rate-limit + honeypot (ref #498)
Phase 7.A.2 + 7.B shipped same-day after Phase 7.A landing (#498 mid-day).

## Live verified on gk2

### Phase 7.A.2

- `packages/secubox-waf/mitmproxy/secubox_waf.py` (older 756→878 lines) :
  backported `_load_crowdsec_cfg`, `_cs_jwt`, `_ban_via_crowdsec`,
  Phase 6.J Connection:close. Both WAF copies now in sync.
- `debian/postinst` : auto-invokes `secubox-waf-cs-bridge-setup` if cscli
  present, installs config to host + bind-mounts into LXC if present.
- `api/routers/waf.py` : new GET /enforcement returns bridge_enabled,
  bans_pushed/failed, requests, blocked, warnings, rate_limit_offenders
  (live nft set count), honeypot_hits_last_hour (from nginx log scan),
  recent_bans (cscli decisions list --origin=secubox-waf), recent_threats.
- `www/mitmproxy/threats.html` : new tab with 6 KPI cards + 2 tables
  (bans, threats), auto-refresh 5s.

### Phase 7.B

- `nftables/secubox-waf-ratelimit.nft` : table inet secubox_waf_ratelimit
  with offenders_v4/v6 (dynamic 5min timeout) + whitelist_v4 (LAN/loopback) +
  input hook priority -10. Rule : tcp SYN to 80/443, limit rate over
  30/second burst 50 → drop + add to offenders. Live-loaded :
  table created, chain attached, ready to drop slowloris/scanners
  the moment they cross threshold.
- `debian/secubox-waf-ratelimit.service` : systemd unit loads the nft
  file on boot (idempotent, RemainAfterExit=yes).
- `nginx/honeypot.conf` : 5 location blocks for known bot signatures
  (`/wp-admin`, `/.env`, `/.git/config`, `/phpmyadmin`, `/actuator` etc).
  Returns empty 200, logs to `/var/log/nginx/honeypot.log` with custom
  `secubox_honeypot` log_format. Live-tested : 3 paths returned 200,
  honeypot.log filled in real-time.
- `debian/postinst` : copies honeypot.conf to /etc/nginx/secubox-routes.d/
  and creates log_format conf.d snippet.

### Files added

  packages/secubox-mitmproxy/nftables/secubox-waf-ratelimit.nft       (NEW)
  packages/secubox-mitmproxy/nginx/honeypot.conf                       (NEW)
  packages/secubox-mitmproxy/www/mitmproxy/threats.html                (NEW)
  packages/secubox-mitmproxy/debian/secubox-waf-ratelimit.service      (NEW)

### Files modified

  packages/secubox-waf/mitmproxy/secubox_waf.py    +120 lines
  packages/secubox-mitmproxy/api/routers/waf.py    +130 lines
  packages/secubox-mitmproxy/debian/postinst       +35 lines
  packages/secubox-mitmproxy/debian/rules          +11 lines

## What's NOT in this commit (Phase 7.C, future)

- eBPF/XDP kernel filter (replaces Python WAF hot-path)
- ModSecurity in HAProxy with OWASP CRS rules
- Federation : push to CrowdSec Hub + pull AlienVault OTX + Spamhaus DROP
- Tune BAN_THRESHOLD per category (XSS=2, SQLi=1)

Filed as remaining sub-tasks in issue #498 (kept open).
2026-06-05 17:23:00 +02:00
70cd718acc docs: Phase 7.A WAF→CrowdSec bridge shipped — HISTORY/WIP/TODO (ref #498)
* HISTORY.md  : 2026-06-05 entry for Phase 7.A (pipeline, files, discoveries)
* WIP.md      : section Phase 7.A done + 7.A.2/7.B/7.C next-ups
* TODO.md     : 7.A checked, 7.A.2 sub-tasks, roadmap doc referenced

Live state on gk2 :
  secubox-cs-bridge.service active (socat 10.100.0.1:8080 → 127.0.0.1:8080)
  cscli machine 'secubox-waf' registered
  mitm WAF reading /etc/secubox/waf/crowdsec.toml + posting alerts on threshold
  Issue #498 stays open for Phase 7.B and 7.C.
2026-06-05 17:13:11 +02:00