From d67e2c03b73f4d7c97d7bc3e4f5cc41c23f4851f Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 20 Jul 2026 17:06:47 +0200 Subject: [PATCH] feat(profiles): package waker/sleeper + lifecycle policy, v0.8.0 (ref #896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the scale-to-zero services into debian/ packaging: secubox-wakectl entry point (/usr/sbin), templates/waking.html now ships, secubox-waker and secubox-sleeper systemd units registered via dh_installsystemd --name=, enabled/restarted (try-restart, preserving runtime state) in postinst alongside secubox-profiles.service. postinst now also runs secubox-wakectl waf-sync/health-sync so sbxwaf and the health monitor have their lists from first install (nginx-sync stays unwired per the 2026-07-20 pivot). Hardens secubox-sleeper.service with ProtectSystem=strict and an explicit ReadWritePaths covering the actuator's real write set, traced through api/actuate.py/snapshot.py/audit.py: /run/secubox, /var/lib/secubox/profiles/rollback, /var/log/secubox, /data/lxc, and /etc/secubox/waf (haproxy-routes.json, written on a routed module's STOP — missed by a naive reading of the write set). Documents the lifecycle/wake_class policy, the waker/sleeper mechanism and a pilot procedure in README.md, wiki/Architecture.md and .claude/MODULE-COMPLIANCE.md. Bumps changelog to 0.8.0. Co-Authored-By: Gerald KERMA --- .claude/MODULE-COMPLIANCE.md | 18 ++++ packages/secubox-profiles/README.md | 96 +++++++++++++++++-- packages/secubox-profiles/debian/changelog | 33 +++++++ packages/secubox-profiles/debian/install | 2 + packages/secubox-profiles/debian/postinst | 36 +++++++ packages/secubox-profiles/debian/prerm | 4 +- packages/secubox-profiles/debian/rules | 12 +++ .../debian/secubox-sleeper.service | 26 +++-- .../secubox-profiles/sbin/secubox-wakectl | 15 +++ wiki/Architecture.md | 32 +++++++ 10 files changed, 256 insertions(+), 18 deletions(-) create mode 100755 packages/secubox-profiles/sbin/secubox-wakectl diff --git a/.claude/MODULE-COMPLIANCE.md b/.claude/MODULE-COMPLIANCE.md index 5a6a6f92..1f1859c4 100644 --- a/.claude/MODULE-COMPLIANCE.md +++ b/.claude/MODULE-COMPLIANCE.md @@ -91,6 +91,24 @@ packages/secubox-/ └── README.md # Module documentation ``` +### Lifecycle Policy — scale-to-zero (ref #896) + +Every module known to `secubox-profiles` (its manifest under +`/etc/secubox/modules.d/.toml`) declares a `lifecycle`: `always-on` +(never stopped — forced for any `protected` module regardless of what it +declares), `eager` (starts at boot, may be idled), `on-demand` (off by +default, woken on first access), or `manual` (operator-only, via the +`/profiles/` panel). An optional `wake_class` (`normal`/`urgent`) tunes the +idle threshold and the wake-splash budget. This is a **module-wide** +guideline, not a `secubox-profiles`-internal detail: a module choosing +`on-demand`/`eager` is opting into being stopped by `secubox-sleeper.service` +and woken via `secubox-waker.service` (fronted by `sbxwaf`, which proxies a +request for a routeless on-demand vhost to the waker instead of returning +421). See `packages/secubox-profiles/README.md` (§ Scale-to-zero) and +`wiki/Architecture.md` (§ Scale-to-zero) for the full policy table, the +wake/sleep mechanism, and the pilot procedure for switching a module to +`on-demand`. + ### Web UI Requirements (Pattern 12) All module frontends MUST include: diff --git a/packages/secubox-profiles/README.md b/packages/secubox-profiles/README.md index b9d67c89..def64863 100644 --- a/packages/secubox-profiles/README.md +++ b/packages/secubox-profiles/README.md @@ -1,6 +1,8 @@ # secubox-profiles Inventaire et profils des modules SecuBox. **Phase 1 : lecture seule.** +**0.4.0+ : actuation (apply/rollback). 0.8.0 (ref #896) : scale-to-zero +(lifecycle/wake_class, waker, sleeper).** ## Commandes @@ -68,6 +70,11 @@ toutes les routes (`Depends(require_jwt)`). | GET | `/api/v1/profiles/diff?profile=` | plan de changements (**n'applique rien**) — 404 si profil inconnu, 409 si `ProtectedViolation` | | POST | `/api/v1/profiles/profiles/{name}/members` | `{"id","on"}` — ajoute/retire un module de la liste `on` d'un profil (404 id/profil inconnu) | | POST | `/api/v1/profiles/pins` | `{"id","pin":"on"\|"off"\|null}` — pose/lève un pin ; **409 et rien d'écrit** si `pin="off"` sur un module protégé | +| POST | `/api/v1/profiles/active` | `{"name"}` — change le profil actif (fichier `active`, pas d'actuation) | +| POST | `/api/v1/profiles/apply` | applique le profil actif (webui→ctl : sudo `secubox-profilectl apply --yes --json`) — snapshot 4R + audit, rollback auto si échec | +| POST | `/api/v1/profiles/rollback` | recharge le dernier snapshot 4R (webui→ctl : `secubox-profilectl rollback --yes --json`) | +| POST | `/api/v1/profiles/wake` | `{"id"}` — réveille manuellement UN module on-demand (webui→ctl, **synchrone** : `secubox-wakectl wake --json`) | +| POST | `/api/v1/profiles/sleep` | `{"id"}` — endort manuellement UN module sleepable (webui→ctl : `secubox-profilectl apply --only --yes --json`) | Écritures limitées à `/etc/secubox/profiles/.toml` et `pins.toml` (atomique : fichier temporaire + `os.replace`) — c'est ce qui rend Phase 1 @@ -82,19 +89,88 @@ Panel webui : `/profiles/` (hybrid-dark, cf. `.claude/WEBUI-PANEL-GUIDELINES.md` ## Manifeste - id = "peertube" - category = "media" # media|security|network|infra|dev|mesh - runtime = "lxc" # native|lxc - exposure = "public" # public|lan|internal - units = ["secubox-peertube.service"] - lxc = "peertube" - portal = { domain = "peertube.gk2.secubox.in" } - priority = 40 # 0-100 - protected = false # true = jamais éteignable - needs = ["auth"] + id = "peertube" + category = "media" # media|security|network|infra|dev|mesh + runtime = "lxc" # native|lxc + exposure = "public" # public|lan|internal + units = ["secubox-peertube.service"] + lxc = "peertube" + portal = { domain = "peertube.gk2.secubox.in" } + priority = 40 # 0-100 + protected = false # true = jamais éteignable + needs = ["auth"] + lifecycle = "on-demand" # always-on|eager|on-demand|manual (défaut: eager) + wake_class = "normal" # normal|urgent (défaut: normal) Un manifeste corrigé à la main fait autorité : `scan` ne l'écrase pas sans `--force`. +## Scale-to-zero — lifecycle, waker, sleeper (ref #896) + +Chaque module déclare un `lifecycle` dans son manifeste +(`/etc/secubox/modules.d/.toml`) : + +| `lifecycle` | Démarre au boot ? | Rendormi si idle ? | Réveillé sur accès ? | +|---|---|---|---| +| `always-on` | oui, toujours | jamais | — (jamais éteint) | +| `eager` | oui | oui (si idle et sans réponse `/idle`) | oui (déjà up en général) | +| `on-demand` | non | oui | oui — `sbxwaf` proxy vers le waker | +| `manual` | non | non | non — opérateur uniquement (`/wake` panel) | + +Un module `protected = true` est **toujours** `always-on`, quoi que déclare +son `lifecycle` (`effective_lifecycle`, `api/lifecycle.py`) — le cœur ne dort +jamais. `wake_class = "urgent"` multiplie le seuil d'inactivité avant sommeil +(×4) et réduit le budget de réveil affiché (15s au lieu de 45s) — pour les +modules qu'on préfère garder chauds plus longtemps mais réveiller vite s'ils +dorment quand même. + +**Le sommeil (`secubox-sleeper.service`, daemon root, tick 30s)** — construit +un plan STOP pour chaque module `eager`/`on-demand` observé up, idle depuis +`idle_threshold()`, sans connexion active, et sans verrou de réveil en cours +(`/run/secubox/waker-active.json`) — et le confie à l'actionneur 0.7.0 +(`apply_plan` : snapshot 4R, audit, timeout dérivé, rollback si échec). Ne +dort **jamais** sur un signal indéterminé (fichier de signaux absent/vide, +sonde `/idle` muette) — l'incertitude protège toujours l'état "up". + +**Le réveil (`secubox-waker.service`, `User=secubox`, socket +`/run/secubox/waker.sock`)** — `sbxwaf` (Go, `packages/secubox-toolbox-ng`) +proxy une requête vers un vhost on-demand sans route active vers +`/_wake/` au lieu de répondre 421. Le waker résout vhost→module, pose +un verrou par module (une requête déclenche le réveil, N requêtes +concurrentes attendent le même réveil), tire `sudo→systemd-run→ +secubox-wakectl wake ` (fire-and-forget, webui→ctl — le waker ne pilote +jamais systemd/LXC en direct) et sert le splash `templates/waking.html` +(503 + `Retry-After`, palette hybrid-dark, auto-refresh) le temps du réveil. + +**Config générée pour les consommateurs** (`secubox-wakectl waf-sync` / +`health-sync`, exécutés au postinst et re-exécutables à la main après un +`scan`) : + +| Fichier | Consommateur | Contenu | +|---|---|---| +| `/etc/secubox/waf/on-demand-vhosts.json` | `sbxwaf` | domaines portail des modules `eager`/`on-demand` routés — décide "proxy vers le waker" vs 421 | +| `/etc/secubox/health/sleepable-modules.json` | `secubox-hub` (moniteur santé) | ids des modules sleepables — distingue "volontairement endormi" de "en panne" | + +`api/nginxgen.py`/`nginx-sync` (Tâche 7) restent dans l'arbre mais **ne sont +plus câblés** — PIVOT (2026-07-20) : le déclencheur de réveil est `sbxwaf`, +pas un `location @waker` nginx. + +**Procédure pilote** (un module `on-demand` à la fois, avant généralisation) : + +1. Éditer `/etc/secubox/modules.d/.toml` : `lifecycle = "on-demand"` + (et `wake_class` si le module doit rester chaud plus longtemps). +2. `secubox-wakectl waf-sync && secubox-wakectl health-sync` (ou attendre le + prochain postinst — mais après une simple édition manuelle du manifeste, + régénérer à la main pour que `sbxwaf` voie le nouveau vhost tout de suite). +3. `secubox-profilectl apply --only --yes --json` pour l'endormir une + première fois (ou attendre le prochain tick idle du sleeper). +4. Visiter le vhost à froid : la première requête doit voir le splash 503 + puis, après `wake_budget()` secondes, le service réel. +5. Surveiller `/var/log/secubox/audit.log` (entrées `wake-on-access` / + `idle-sleep`) et `journalctl -u secubox-waker -u secubox-sleeper`. +6. `lifecycle = "manual"` si le pilote révèle un réveil trop lent/fragile + pour un accès non annoncé — le module reste éteignable/allumable + seulement depuis le panel. + ## Tests python3 -m pytest packages/secubox-profiles/tests -q diff --git a/packages/secubox-profiles/debian/changelog b/packages/secubox-profiles/debian/changelog index 701617a8..474cf79a 100644 --- a/packages/secubox-profiles/debian/changelog +++ b/packages/secubox-profiles/debian/changelog @@ -1,3 +1,36 @@ +secubox-profiles (0.8.0-1~bookworm1) bookworm; urgency=medium + + * Scale-to-zero (ref #896): ships the lifecycle/wake_class manifest policy + (always-on/eager/on-demand/manual; normal/urgent — api/lifecycle.py), + the secubox-waker activator (wake-on-access splash + one-wake lock, + webui->ctl via systemd-run) and the secubox-sleeper idle daemon (idle + detection off sbxwaf per-vhost signals, apply_plan-driven STOP, never + sleeps on an indeterminate signal). + * sbxwaf-side wake trigger (packages/secubox-toolbox-ng, packages/secubox-waf-ng): + on-demand vhosts without an active route now proxy to the waker instead + of 421 — supersedes the nginx `@waker` path (Task 7, now unwired; + api/nginxgen.py stays in the tree, dead code, for a possible future + dual-front-end). New `secubox-wakectl waf-sync`/`health-sync` write + /etc/secubox/waf/on-demand-vhosts.json and + /etc/secubox/health/sleepable-modules.json — run at postinst so sbxwaf + and the health monitor have their lists from first install. + * webui: `/profiles/` panel gains manual sleep/wake buttons (POST + /wake, /sleep) alongside the existing lifecycle column in `/status`. + * secubox-watchdog awareness: a sleeping eager/on-demand module is a + DELIBERATE state, never auto-revived (watchdog_should_manage). + * Packaging: new `secubox-waker.service` + `secubox-sleeper.service` units + (enable --now + try-restart in postinst, preserving runtime state on + upgrade), new `secubox-wakectl` entry point (/usr/sbin), templates/ + now ships (waking.html — the splash the waker serves). Sudoers grants + for the waker/sleeper's webui->ctl calls were already shipped in 0.7.x. + secubox-sleeper.service (the one unit that drives the actuator + in-process, unlike waker/profiles which delegate via systemd-run) now + runs ProtectSystem=strict with an explicit ReadWritePaths= covering the + actuator's real write set (snapshot 4R, audit log, LXC autostart config, + HAProxy portal routes) instead of an unconfined root process. + + -- Gerald KERMA Mon, 20 Jul 2026 17:03:19 +0200 + secubox-profiles (0.7.0-1~bookworm1) bookworm; urgency=medium * apply: observed state (not command return code) arbitrates STOP/START — diff --git a/packages/secubox-profiles/debian/install b/packages/secubox-profiles/debian/install index 75ccd77b..78988fd1 100644 --- a/packages/secubox-profiles/debian/install +++ b/packages/secubox-profiles/debian/install @@ -1,6 +1,8 @@ api/*.py usr/lib/secubox/profiles/api/ +templates/waking.html usr/lib/secubox/profiles/templates/ profiles/*.toml usr/share/secubox/profiles/ sbin/secubox-profilectl usr/sbin/ +sbin/secubox-wakectl usr/sbin/ www/profiles/index.html usr/share/secubox/www/profiles/ menu.d/11-profiles.json usr/share/secubox/menu.d/ nginx/profiles.conf etc/nginx/secubox-routes.d/ diff --git a/packages/secubox-profiles/debian/postinst b/packages/secubox-profiles/debian/postinst index df81c39a..b296a191 100755 --- a/packages/secubox-profiles/debian/postinst +++ b/packages/secubox-profiles/debian/postinst @@ -25,12 +25,48 @@ case "$1" in fi done + # secubox-sleeper.service runs ProtectSystem=strict with an explicit + # ReadWritePaths= (see debian/secubox-sleeper.service) — every + # directory it lists MUST already exist before the unit's first + # start, or systemd's sandbox will not make it writable. Pre-create + # this package's own two leaves here (shared parents /var/lib/secubox + # and /var/log/secubox themselves come from secubox-core, 0755 + # secubox:secubox — reasserted here matching the repo-wide + # shared-parent convention, never narrowed to 0750). /run/secubox + # comes from RuntimeDirectory=secubox; /data/lxc and /etc/secubox/waf + # are provisioned by lxc/secubox-waf-ng respectively, not here. + install -d -o secubox -g secubox -m 0755 /var/log/secubox + install -d -o root -g root -m 0750 /var/lib/secubox/profiles/rollback + # Own dedicated socket, own service — never served by the aggregator # (the profile engine must not depend on something it may later # restart). Restart (not just enable) so an upgrade picks up new code. systemctl daemon-reload 2>/dev/null || true systemctl enable secubox-profiles.service 2>/dev/null || true systemctl restart secubox-profiles.service 2>/dev/null || true + + # secubox-waker + secubox-sleeper (ref #896, scale-to-zero): `enable + # --now` is safe/idempotent (no-op if already enabled+running — the + # first-install case starts them), `try-restart` afterwards is a + # no-op if inactive and gracefully restarts (not a hard kill) an + # ALREADY-running instance so an upgrade picks up new code without + # forcibly disrupting an in-flight wake/sleep decision. + systemctl enable --now secubox-waker.service secubox-sleeper.service 2>/dev/null || true + systemctl try-restart secubox-waker.service secubox-sleeper.service 2>/dev/null || true + + # Config sync for the sbxwaf-side wake trigger + the health monitor + # (ref #896 — supersedes the nginx-sync path of Task 7, PIVOT + # 2026-07-20): waf-sync writes the on-demand vhost list sbxwaf reads + # to decide "proxy to the waker instead of 421"; health-sync writes + # the sleepable-module-id set secubox-hub reads to distinguish + # "voluntarily asleep" from "down". Both are idempotent, best-effort + # (an empty modules.d before the first `scan` just yields an empty + # list — never a postinst failure). + [ -d /etc/secubox/waf ] || install -d -m 0750 /etc/secubox/waf + install -d -o root -g root -m 0755 /etc/secubox/health + /usr/sbin/secubox-wakectl waf-sync 2>/dev/null || true + /usr/sbin/secubox-wakectl health-sync 2>/dev/null || true + systemctl reload nginx 2>/dev/null || true ;; esac diff --git a/packages/secubox-profiles/debian/prerm b/packages/secubox-profiles/debian/prerm index a9c46d30..0b8bbd32 100755 --- a/packages/secubox-profiles/debian/prerm +++ b/packages/secubox-profiles/debian/prerm @@ -1,8 +1,8 @@ #!/bin/sh set -e if [ "$1" = "remove" ]; then - systemctl stop secubox-profiles.service || true - systemctl disable secubox-profiles.service || true + systemctl stop secubox-profiles.service secubox-waker.service secubox-sleeper.service || true + systemctl disable secubox-profiles.service secubox-waker.service secubox-sleeper.service || true systemctl reload nginx 2>/dev/null || true fi #DEBHELPER# diff --git a/packages/secubox-profiles/debian/rules b/packages/secubox-profiles/debian/rules index 19bb36bf..b5d1a900 100755 --- a/packages/secubox-profiles/debian/rules +++ b/packages/secubox-profiles/debian/rules @@ -8,3 +8,15 @@ override_dh_auto_install: install -d debian/secubox-profiles/etc/sudoers.d install -m 0440 sudoers.d/secubox-profiles debian/secubox-profiles/etc/sudoers.d/secubox-profiles + +# Three distinct units ship in this package (own socket each — see +# debian/secubox-profiles.service, debian/secubox-waker.service, +# debian/secubox-sleeper.service): dh_installsystemd's default naming only +# auto-picks up debian/.service, so the other two need an explicit +# --name= each. Default enable+start (no --no-enable) matches how +# secubox-profiles.service already behaves (postinst also enables/restarts +# it manually — same, deliberately redundant-but-safe pattern kept below). +override_dh_installsystemd: + dh_installsystemd --name=secubox-profiles + dh_installsystemd --name=secubox-waker + dh_installsystemd --name=secubox-sleeper diff --git a/packages/secubox-profiles/debian/secubox-sleeper.service b/packages/secubox-profiles/debian/secubox-sleeper.service index a91bed7f..0c5c3c1c 100644 --- a/packages/secubox-profiles/debian/secubox-sleeper.service +++ b/packages/secubox-profiles/debian/secubox-sleeper.service @@ -22,19 +22,33 @@ UMask=0027 # hands each idle module straight to api.apply.apply_plan, the SAME code # path as secubox-wakectl/secubox-profilectl apply/rollback — same trust # level, just driven by a timer loop instead of an operator or a wake -# request. No ProtectSystem=strict either: apply_plan drives -# systemctl/lxc-start/lxc-stop AND edits /data/lxc//config -# (lxc.start.auto) and /etc/secubox/waf/haproxy-routes.json directly (see -# api/actuate.py) — sandboxing this service would just reproduce, one layer -# up, the exact EROFS lesson that motivated wrapping the OTHER two ctls in -# systemd-run in the first place. +# request. NoNewPrivileges=true RuntimeDirectory=secubox RuntimeDirectoryPreserve=yes RuntimeDirectoryMode=0775 +# ProtectSystem=strict makes / read-only by default; the actuator's actual +# write set (traced through api/actuate.py, api/snapshot.py, api/audit.py — +# apply_plan's STOP path is the only one the sleeper ever drives) is listed +# explicitly below so this sandbox stays FUNCTIONAL, not just hardened: +# - /run/secubox — the socket dir + api.sleeper.WAKE_LOCK_FILE +# (read, but ReadWritePaths is directory-granular) and waker-active.json +# - /var/lib/secubox/profiles/rollback — snapshot.SNAP_DIR, the 4R chain +# shared with secubox-profilectl/secubox-wakectl (actuate_paths.py) +# - /var/log/secubox — audit.AUDIT_LOG (append-only audit.log) +# - /data/lxc — actuate.py::_lxc_autostart edits +# //config (lxc.start.auto) before lxc-stop +# - /etc/secubox/waf — actuate.py::ROUTES_FILE +# (haproxy-routes.json): a STOP on a routed module calls +# _portal_remove, which rewrites this file (temp+rename in the SAME +# directory) — omitting it here would reproduce, one layer up, the +# exact EROFS lesson that motivated wrapping the OTHER two ctls in +# systemd-run in the first place (secubox-profilectl 0.6.1). +ProtectSystem=strict ProtectHome=true PrivateTmp=true +ReadWritePaths=/run/secubox /var/lib/secubox/profiles/rollback /var/log/secubox /data/lxc /etc/secubox/waf [Install] WantedBy=multi-user.target diff --git a/packages/secubox-profiles/sbin/secubox-wakectl b/packages/secubox-profiles/sbin/secubox-wakectl new file mode 100755 index 00000000..050db0fe --- /dev/null +++ b/packages/secubox-profiles/sbin/secubox-wakectl @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma +# Source-Disclosed License — All rights reserved except as expressly granted. +# See LICENCE-CMSD-1.0.md for terms. + +# SecuBox-Deb :: secubox-wakectl (ref #896) — réveille un module on-demand +# (wake), ou (re)génère les listes de config consommées par sbxwaf/health +# (nginx-sync, waf-sync, health-sync). Même motif que secubox-profilectl : +# wrapper mince, toute la logique vit dans api/wake.py. +set -euo pipefail +readonly MODULE="profiles" +readonly VERSION="0.8.0" +cd /usr/lib/secubox/profiles +exec /usr/bin/python3 -m api.wake "$@" diff --git a/wiki/Architecture.md b/wiki/Architecture.md index 0dd4b5de..711da330 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -173,6 +173,38 @@ Détails complets : [`docs/MODULE-GUIDELINES.md`](../docs/MODULE-GUIDELINES.md) --- +## Scale-to-zero — cycle de vie des modules (ref #896) + +`secubox-profiles` porte, en plus de l'inventaire/apply déjà décrits, une +politique de cycle de vie par module (`lifecycle` dans son manifeste +`/etc/secubox/modules.d/.toml`) : `always-on` (jamais éteint — un +`protected` l'est toujours, quoi qu'il déclare), `eager` (démarre au boot, +peut se rendormir), `on-demand` (éteint par défaut, réveillé sur accès) et +`manual` (opérateur uniquement). + +Deux services dédiés pilotent le sommeil/réveil : + +- **`secubox-sleeper.service`** (root, tick 30s) — endort tout module + `eager`/`on-demand` observé idle (aucune connexion active depuis le seuil + configuré, aucun réveil en cours) via le même actionneur `apply_plan` que + `secubox-profilectl apply` (snapshot 4R, audit, rollback si échec). Ne dort + jamais sur un signal indéterminé. +- **`secubox-waker.service`** (`User=secubox`, non privilégié) — reçoit du + trafic **sbxwaf** (le WAF Go, `packages/secubox-toolbox-ng`/`secubox-waf-ng`) : + un vhost on-demand sans route active est proxifié vers `/_wake/` au + lieu d'un 421. Le waker sert un splash 503 (`templates/waking.html`) et + déclenche le réveil via `sudo→systemd-run→secubox-wakectl` (webui→ctl, + jamais en direct — même patron que les autres `ctl` confinés). + +Ce mécanisme **remplace** le déclencheur nginx `@waker` envisagé initialement +(PIVOT 2026-07-20) : `sbxwaf` est le point d'entrée du trafic public sur ce +dépôt, c'est donc lui qui détecte l'accès à un vhost éteint, pas nginx. + +Détails complets (table lifecycle, format manifeste, procédure pilote) : +[`packages/secubox-profiles/README.md`](../packages/secubox-profiles/README.md#scale-to-zero--lifecycle-waker-sleeper-ref-896). + +--- + ## Contraintes ANSSI CSPN Le projet vise la certification ANSSI CSPN à horizon 2027. Contraintes respectées :