mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Merge pull request #884 from CyberMind-FR/fix/cve-triage-panel-apply-sudo
Some checks are pending
License Headers / check (push) Waiting to run
Some checks are pending
License Headers / check (push) Waiting to run
fix(cve-triage): WAF panel delegates to root secubox-cvectl + webui→ctl guideline (1.1.2)
This commit is contained in:
commit
753524c38a
|
|
@ -154,6 +154,72 @@ API MUST listen on Unix socket: `/run/secubox/<module>.sock`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Privileged Operations — webui delegates to a confined, audited `ctl` (Required)
|
||||||
|
|
||||||
|
**Principle.** The webui/API runs **unprivileged** — `User=secubox`, and when
|
||||||
|
aggregator-served it shares the aggregator's `secubox` context. It therefore
|
||||||
|
**cannot** read/write root-owned config (e.g. `/etc/secubox/waf` is `0750
|
||||||
|
root:root`), and **must not** drive systemd / LXC / applications in-process.
|
||||||
|
|
||||||
|
Every operation that (a) touches root-owned files, or (b) pilots the system or
|
||||||
|
another app (start/stop/reload a unit, edit a live config, run a privileged
|
||||||
|
CLI) **MUST be delegated to the module's root helper** `secubox-<module>ctl`.
|
||||||
|
The webui becomes a thin JWT client; the **`ctl` is the single privileged
|
||||||
|
surface** — confined (scoped sudoers), auditable (it logs each action to
|
||||||
|
`/var/log/secubox/audit.log` for security-relevant changes), and it is what
|
||||||
|
actually causes the system/apps to change. Doing the privileged work in-process
|
||||||
|
raises `PermissionError` → HTTP 500 ("request error" / empty panel) and
|
||||||
|
bypasses the audit trail.
|
||||||
|
|
||||||
|
**Sudoers grant (ship it — a missing grant is a compliance failure).**
|
||||||
|
Ship `sudoers.d/secubox-<module>` and install it `0440` to
|
||||||
|
`/etc/sudoers.d/secubox-<module>` from `debian/rules`. Every grant is an
|
||||||
|
**exact-command** match — no wildcards, no shell, no flag escapes — and each is
|
||||||
|
**documented** (which route uses it, why root):
|
||||||
|
|
||||||
|
```
|
||||||
|
# secubox ALL=(root) NOPASSWD: <absolute-path> <exact args...>
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-cvectl waf-rules generate --json
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-cvectl waf-rules generate --apply --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate with `visudo -c -f sudoers.d/secubox-<module>` in CI/before deploy.
|
||||||
|
|
||||||
|
**Panel side.** Call the helper over `sudo -n` from a **plain `def`** handler
|
||||||
|
(so the blocking subprocess runs in FastAPI's threadpool, off the shared
|
||||||
|
aggregator loop), and map the `ctl` exit code to an HTTP status:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _run_ctl(*args):
|
||||||
|
import subprocess
|
||||||
|
return subprocess.run(["sudo", "-n", "/usr/sbin/secubox-<module>ctl", *args],
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
|
|
||||||
|
@app.post("/<action>", dependencies=[Depends(require_jwt)])
|
||||||
|
def do_action():
|
||||||
|
import json
|
||||||
|
p = _run_ctl("<verb>", "--apply", "--json")
|
||||||
|
if p.returncode == 3: # module-defined fail-safe refusal
|
||||||
|
raise HTTPException(status_code=409, detail="…")
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise HTTPException(status_code=500, detail=(p.stderr or p.stdout).strip()[:500])
|
||||||
|
return json.loads(p.stdout) # ctl emits a --json payload the panel renders
|
||||||
|
```
|
||||||
|
|
||||||
|
**The `ctl` contract.** Runs as root; validates its own inputs; performs the
|
||||||
|
privileged action; supports a **dry-run default** and an explicit `--apply` for
|
||||||
|
any state change; offers a machine-readable `--json` output for the panel;
|
||||||
|
appends an audit line for each security-relevant decision. Reference
|
||||||
|
implementations: `secubox-cvectl` (WAF rule generation), `secubox-profilectl`
|
||||||
|
(module on/off actuation).
|
||||||
|
|
||||||
|
**Aggregator note.** An aggregator-served module's route code is imported at
|
||||||
|
aggregator startup — after deploying new/changed routes you MUST
|
||||||
|
`systemctl restart secubox-aggregator` for them to appear (a stale aggregator
|
||||||
|
returns 404 on new routes). Modules on their own socket restart independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Debian Package Requirements
|
## Debian Package Requirements
|
||||||
|
|
||||||
### debian/control
|
### debian/control
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,15 @@ Emoji are functional glyphs, not decoration — one per concept, consistent:
|
||||||
rows. Cache/stale-while-revalidate on the server for expensive endpoints.
|
rows. Cache/stale-while-revalidate on the server for expensive endpoints.
|
||||||
- **Destructive actions confirm()** — and warn explicitly when an action can cut
|
- **Destructive actions confirm()** — and warn explicitly when an action can cut
|
||||||
the operator's own access (see wireguard admin/mesh bring-down guard).
|
the operator's own access (see wireguard admin/mesh bring-down guard).
|
||||||
|
- **Privileged actions delegate to the module `ctl` — never do them in-process.**
|
||||||
|
The panel runs unprivileged (`secubox`); anything that writes root-owned config
|
||||||
|
or drives systemd/LXC/an app must POST to a route that shells out to
|
||||||
|
`sudo -n /usr/sbin/secubox-<module>ctl …` (scoped exact-command sudoers), which
|
||||||
|
performs and **audits** the change as root and returns a `--json` payload the
|
||||||
|
panel renders. In-process privileged work raises `PermissionError` → a 500 the
|
||||||
|
user sees as "request error"/empty panel. Full contract (sudoers, `ctl`
|
||||||
|
responsibilities, aggregator-restart caveat): **`.claude/MODULE-COMPLIANCE.md`
|
||||||
|
→ Privileged Operations**. Reference: `cve-triage` / `secubox-cvectl`.
|
||||||
|
|
||||||
## 7. Checklist for a new/reskinned panel
|
## 7. Checklist for a new/reskinned panel
|
||||||
|
|
||||||
|
|
@ -176,6 +185,7 @@ Emoji are functional glyphs, not decoration — one per concept, consistent:
|
||||||
- [ ] `esc()` on all injected values, `data-*` delegation, no onclick interpolation
|
- [ ] `esc()` on all injected values, `data-*` delegation, no onclick interpolation
|
||||||
- [ ] responsive `@media (max-width:768px)`, body never scrolls sideways
|
- [ ] responsive `@media (max-width:768px)`, body never scrolls sideways
|
||||||
- [ ] menu.d entry present (navbar auto-renders it)
|
- [ ] menu.d entry present (navbar auto-renders it)
|
||||||
|
- [ ] privileged actions routed through `sudo secubox-<module>ctl` (scoped sudoers shipped), never done in-process
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,34 @@ The `build-image.sh` slipstream loop (`cp /tmp/secubox-debs/secubox-*.deb`) pick
|
||||||
Read first: [`docs/grammar.md`](grammar.md) (canonical verbs table) +
|
Read first: [`docs/grammar.md`](grammar.md) (canonical verbs table) +
|
||||||
[`HOWTO-grammar.md`](../HOWTO-grammar.md) (recipe for adding a verb).
|
[`HOWTO-grammar.md`](../HOWTO-grammar.md) (recipe for adding a verb).
|
||||||
|
|
||||||
|
### Privilege & delegation — the `ctl` is the single privileged, audited surface
|
||||||
|
|
||||||
|
The WebUI/API runs **unprivileged** (`User=secubox`; aggregator-served modules
|
||||||
|
share that context). It cannot touch root-owned config (e.g. `/etc/secubox/waf`
|
||||||
|
is `0750 root:root`) and must not drive systemd/LXC/apps in-process. Every
|
||||||
|
privileged operation — anything that **causes the system or an app to change**,
|
||||||
|
or reads/writes root-owned state — is **delegated to `<module>ctl`**, which runs
|
||||||
|
as root, validates its inputs, performs the change, and **audits** each
|
||||||
|
security-relevant decision (`/var/log/secubox/audit.log`).
|
||||||
|
|
||||||
|
The panel reaches it over `sudo -n`, gated by a **scoped, exact-command** grant
|
||||||
|
the package ships (`sudoers.d/secubox-<module>`, installed `0440` to
|
||||||
|
`/etc/sudoers.d/secubox-<module>` — no wildcards, no flag escapes, one line per
|
||||||
|
allowed invocation, each documented). The `ctl` offers a `--json` output for the
|
||||||
|
panel and a **dry-run default / explicit `--apply`** for any state change.
|
||||||
|
|
||||||
|
```text
|
||||||
|
# sudoers.d/secubox-<module> (0440, exact-command; validated with visudo -c)
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-<module>ctl <verb> --json
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-<module>ctl <verb> --apply --json
|
||||||
|
```
|
||||||
|
|
||||||
|
A missing grant is a compliance failure: the panel then does the work
|
||||||
|
in-process, hits `PermissionError` → HTTP 500 (operator sees "request error" /
|
||||||
|
an empty panel) and bypasses the audit trail. Full contract:
|
||||||
|
[`.claude/MODULE-COMPLIANCE.md`](../.claude/MODULE-COMPLIANCE.md) → *Privileged
|
||||||
|
Operations*. Reference: `secubox-cvectl`, `secubox-profilectl`.
|
||||||
|
|
||||||
### Mandatory three-fold
|
### Mandatory three-fold
|
||||||
|
|
||||||
Every `<module>ctl` exposes:
|
Every `<module>ctl` exposes:
|
||||||
|
|
|
||||||
|
|
@ -1100,54 +1100,70 @@ async def list_kev_cves():
|
||||||
# ~110 modules — a blocking call inside an `async def` handler would freeze
|
# ~110 modules — a blocking call inside an `async def` handler would freeze
|
||||||
# the whole board. Plain `def` handlers run in FastAPI's threadpool instead.
|
# the whole board. Plain `def` handlers run in FastAPI's threadpool instead.
|
||||||
|
|
||||||
|
def _run_cvectl_generate(*extra: str):
|
||||||
|
"""Run the root generator CLI over sudo (exact-command sudoers grant — see
|
||||||
|
/etc/sudoers.d/secubox-cve-triage). The panel runs as `secubox` in the
|
||||||
|
aggregator, but /etc/secubox/waf is 0750 root:root, so the whole generate
|
||||||
|
flow (reading the WAF routes for the presence inventory, reading/writing
|
||||||
|
waf-rules.json) is root-only. Handlers are plain `def` so this blocking
|
||||||
|
subprocess runs in FastAPI's threadpool, off the shared aggregator loop."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
return subprocess.run(
|
||||||
|
["sudo", "-n", "/usr/sbin/secubox-cvectl", "waf-rules", "generate", *extra],
|
||||||
|
capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/waf-rules", dependencies=[Depends(require_jwt)])
|
@app.get("/waf-rules", dependencies=[Depends(require_jwt)])
|
||||||
def waf_rules_preview():
|
def waf_rules_preview():
|
||||||
"""Dry-run: what would be generated (kept + rejections). Writes nothing."""
|
"""Dry-run preview (kept + rejections), via the root CLI. Writes nothing."""
|
||||||
from .wafgen.generate import generate
|
import json
|
||||||
from .wafgen.inventory import gather_present
|
|
||||||
from .wafgen.emit import existing_patterns
|
|
||||||
|
|
||||||
present, complete = gather_present()
|
proc = _run_cvectl_generate("--json")
|
||||||
kept, rejected = generate(
|
if proc.returncode != 0:
|
||||||
Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete,
|
raise HTTPException(
|
||||||
existing=existing_patterns(Path("/etc/secubox/waf/waf-rules.json")),
|
status_code=500,
|
||||||
)
|
detail=f"waf-rules preview failed: {(proc.stderr or proc.stdout).strip()[:500]}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = json.loads(proc.stdout)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=500, detail="waf-rules preview: malformed CLI output")
|
||||||
return {
|
return {
|
||||||
"present_count": len(present),
|
"present_count": data.get("present_count", 0),
|
||||||
"inventory_complete": complete,
|
"inventory_complete": data.get("complete", False),
|
||||||
"kept": [
|
"kept": data.get("kept", []),
|
||||||
{"cve": c.cve, "vendor": c.vendor, "product": c.product, "path": c.path}
|
"rejected": data.get("rejected", []),
|
||||||
for c in kept
|
|
||||||
],
|
|
||||||
"rejected": [{"file": n, "reason": r} for n, r in rejected],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/waf-rules/generate", dependencies=[Depends(require_jwt)])
|
@app.post("/waf-rules/generate", dependencies=[Depends(require_jwt)])
|
||||||
def waf_rules_generate():
|
def waf_rules_generate():
|
||||||
"""Apply: write product_absent_probes (detect mode). Refuses if the
|
"""Apply the product_absent_probes detect category via the root CLI (sudo).
|
||||||
presence inventory is incomplete (fail-safe)."""
|
Refuses (409) when the presence inventory is incomplete (CLI exit 3)."""
|
||||||
from .wafgen.generate import generate
|
import json
|
||||||
from .wafgen.inventory import gather_present
|
|
||||||
from .wafgen.emit import existing_patterns, write_category
|
|
||||||
|
|
||||||
present, complete = gather_present()
|
proc = _run_cvectl_generate("--apply", "--json")
|
||||||
if not complete:
|
if proc.returncode == 3:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
detail="presence inventory incomplete — refusing (fail-safe)",
|
detail="presence inventory incomplete — refusing (fail-safe)",
|
||||||
)
|
)
|
||||||
kept, rejected = generate(
|
if proc.returncode != 0:
|
||||||
Path("/usr/lib/secubox/cve-triage/nuclei-subset"), present, complete,
|
raise HTTPException(
|
||||||
existing=existing_patterns(Path("/etc/secubox/waf/waf-rules.json")),
|
status_code=500,
|
||||||
)
|
detail=f"waf-rules generate failed: {(proc.stderr or proc.stdout).strip()[:500]}",
|
||||||
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
)
|
||||||
write_category(Path("/etc/secubox/waf/waf-rules.json"), kept, now=now)
|
try:
|
||||||
|
data = json.loads(proc.stdout)
|
||||||
|
except ValueError:
|
||||||
|
data = {}
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"written": len(kept),
|
"written": data.get("written", 0),
|
||||||
"mode": "detect",
|
"mode": "detect",
|
||||||
"rejected": len(rejected),
|
"rejected": len(data.get("rejected", [])),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ sans écrire. --apply écrit la catégorie product_absent_probes en mode detect.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -37,6 +38,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
gen = wsub.add_parser("generate", help="generate product-absent probe rules")
|
gen = wsub.add_parser("generate", help="generate product-absent probe rules")
|
||||||
gen.add_argument("--apply", action="store_true",
|
gen.add_argument("--apply", action="store_true",
|
||||||
help="write the category (default: dry-run, write nothing)")
|
help="write the category (default: dry-run, write nothing)")
|
||||||
|
gen.add_argument("--json", action="store_true",
|
||||||
|
help="emit a JSON payload instead of the human text (used by the panel)")
|
||||||
gen.add_argument("--subset", default=str(SUBSET_DIR))
|
gen.add_argument("--subset", default=str(SUBSET_DIR))
|
||||||
gen.add_argument("--rules", default=str(RULES_PATH))
|
gen.add_argument("--rules", default=str(RULES_PATH))
|
||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
|
|
@ -45,22 +48,48 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
kept, rejected = generate(Path(args.subset), present, complete,
|
kept, rejected = generate(Path(args.subset), present, complete,
|
||||||
existing=existing_patterns(Path(args.rules)))
|
existing=existing_patterns(Path(args.rules)))
|
||||||
|
|
||||||
print(f"presence union: {len(present)} product(s), complete={complete}")
|
# Shared payload for --json (the panel routes parse this over sudo).
|
||||||
print(f"kept: {len(kept)} probe(s), rejected: {len(rejected)}")
|
payload = {
|
||||||
for c in kept:
|
"present_count": len(present),
|
||||||
print(f" ✓ {c.cve:<16} {c.vendor}/{c.product} {c.path}")
|
"complete": complete,
|
||||||
for name, why in rejected:
|
"kept": [{"cve": c.cve, "vendor": c.vendor, "product": c.product,
|
||||||
print(f" ✗ {name}: {why}")
|
"path": c.path} for c in kept],
|
||||||
|
"rejected": [{"file": n, "reason": r} for n, r in rejected],
|
||||||
|
"applied": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not args.json:
|
||||||
|
print(f"presence union: {len(present)} product(s), complete={complete}")
|
||||||
|
print(f"kept: {len(kept)} probe(s), rejected: {len(rejected)}")
|
||||||
|
for c in kept:
|
||||||
|
print(f" ✓ {c.cve:<16} {c.vendor}/{c.product} {c.path}")
|
||||||
|
for name, why in rejected:
|
||||||
|
print(f" ✗ {name}: {why}")
|
||||||
|
|
||||||
if not args.apply:
|
if not args.apply:
|
||||||
print("dry-run — nothing written (use --apply to write).")
|
if args.json:
|
||||||
|
print(json.dumps(payload))
|
||||||
|
else:
|
||||||
|
print("dry-run — nothing written (use --apply to write).")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# --apply: refuse when the presence inventory is incomplete (fail-safe).
|
||||||
if not complete:
|
if not complete:
|
||||||
print("refusing to write: presence inventory incomplete (fail-safe).",
|
if args.json:
|
||||||
file=sys.stderr)
|
print(json.dumps({**payload,
|
||||||
|
"error": "presence inventory incomplete — refusing (fail-safe)"}))
|
||||||
|
else:
|
||||||
|
print("refusing to write: presence inventory incomplete (fail-safe).",
|
||||||
|
file=sys.stderr)
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
write_category(Path(args.rules), kept, now=_now())
|
write_category(Path(args.rules), kept, now=_now())
|
||||||
print(f"wrote {len(kept)} probe(s) to product_absent_probes (mode=detect) in {args.rules}")
|
payload["applied"] = True
|
||||||
|
payload["written"] = len(kept)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(payload))
|
||||||
|
else:
|
||||||
|
print(f"wrote {len(kept)} probe(s) to product_absent_probes (mode=detect) in {args.rules}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
secubox-cve-triage (1.1.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Fix the WAF-probes panel: the aggregator-served routes (User=secubox) could
|
||||||
|
not read/write the 0750 root:root /etc/secubox/waf tree — the preview 500'd
|
||||||
|
("waf empty") and Generate returned a request error (PermissionError). Both
|
||||||
|
routes now delegate to the root helper `secubox-cvectl waf-rules generate`
|
||||||
|
(`--json` preview, `--apply --json` write) via scoped, exact-command sudoers
|
||||||
|
grants (/etc/sudoers.d/secubox-cve-triage). Adds `--json` output to the CLI.
|
||||||
|
The webui-delegates-to-a-confined-audited-ctl pattern is now a module
|
||||||
|
guideline (see .claude/MODULE-COMPLIANCE.md + WEBUI-PANEL-GUIDELINES.md).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 19 Jul 2026 09:00:00 +0200
|
||||||
|
|
||||||
secubox-cve-triage (1.1.1-1~bookworm1) bookworm; urgency=medium
|
secubox-cve-triage (1.1.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* WAF generator: skip probes already covered by another WAF category
|
* WAF generator: skip probes already covered by another WAF category
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,7 @@ override_dh_auto_install:
|
||||||
install -m 0755 sbin/secubox-cvectl debian/secubox-cve-triage/usr/sbin/
|
install -m 0755 sbin/secubox-cvectl debian/secubox-cve-triage/usr/sbin/
|
||||||
install -d debian/secubox-cve-triage/usr/lib/secubox/cve-triage/nuclei-subset
|
install -d debian/secubox-cve-triage/usr/lib/secubox/cve-triage/nuclei-subset
|
||||||
cp -r nuclei-subset/. debian/secubox-cve-triage/usr/lib/secubox/cve-triage/nuclei-subset/
|
cp -r nuclei-subset/. debian/secubox-cve-triage/usr/lib/secubox/cve-triage/nuclei-subset/
|
||||||
|
# sudoers: the panel (User=secubox in the aggregator) delegates the
|
||||||
|
# root-only generate flow to secubox-cvectl via exact-command sudo grants.
|
||||||
|
install -d debian/secubox-cve-triage/etc/sudoers.d
|
||||||
|
install -m 0440 sudoers.d/secubox-cve-triage debian/secubox-cve-triage/etc/sudoers.d/secubox-cve-triage
|
||||||
|
|
|
||||||
17
packages/secubox-cve-triage/sudoers.d/secubox-cve-triage
Normal file
17
packages/secubox-cve-triage/sudoers.d/secubox-cve-triage
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# SecuBox CVE-Triage — the WAF product-absent generator panel runs in-process
|
||||||
|
# under the aggregator (User=secubox), but /etc/secubox/waf is 0750 root:root:
|
||||||
|
# secubox can neither traverse it (to read haproxy-routes.json for the presence
|
||||||
|
# inventory, nor waf-rules.json) nor write waf-rules.json. The whole generate
|
||||||
|
# flow is therefore root-only. Both panel operations are routed through the root
|
||||||
|
# helper `secubox-cvectl waf-rules generate` via these scoped, exact-command
|
||||||
|
# grants (no wildcards, no flag escapes) — same pattern as secubox-crowdsec /
|
||||||
|
# secubox-threats.
|
||||||
|
#
|
||||||
|
# GET /api/v1/cve-triage/waf-rules → generate --json (dry-run preview)
|
||||||
|
# POST /api/v1/cve-triage/waf-rules/generate → generate --apply --json (write detect category)
|
||||||
|
#
|
||||||
|
# Operator UI symptom if these entries are missing: the panel's WAF tab shows
|
||||||
|
# empty (preview 500 / PermissionError) and "Generate" returns a request error.
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-cvectl waf-rules generate --json
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-cvectl waf-rules generate --apply --json
|
||||||
|
|
@ -126,6 +126,33 @@ POST /api/v1/<module>/<method> # Action
|
||||||
|
|
||||||
Authentification JWT obligatoire sur tous les endpoints via `Depends(auth.require_jwt)`.
|
Authentification JWT obligatoire sur tous les endpoints via `Depends(auth.require_jwt)`.
|
||||||
|
|
||||||
|
### Opérations privilégiées — le webui sous-traite au `ctl` confiné et audité (REQUIRED)
|
||||||
|
|
||||||
|
L'API/webui tourne **sans privilège** (`User=secubox`, et servie in-process par
|
||||||
|
l'aggregator elle partage son contexte `secubox`). Elle ne peut donc ni lire/écrire
|
||||||
|
la config root (ex. `/etc/secubox/waf` est `0750 root:root`), ni piloter
|
||||||
|
systemd/LXC/une app en direct.
|
||||||
|
|
||||||
|
> **Principe.** Toute opération qui touche un fichier root ou qui **cause au
|
||||||
|
> système / à une app** (start/stop/reload d'une unit, écriture d'une config live,
|
||||||
|
> exécution d'un binaire privilégié) est **déléguée au helper root
|
||||||
|
> `secubox-<module>ctl`**. Le webui devient un client JWT léger ; le **`ctl` est la
|
||||||
|
> surface privilégiée unique** — confinée (sudoers scopé, commande exacte),
|
||||||
|
> **auditée** (`/var/log/secubox/audit.log`), et c'est elle qui pilote réellement.
|
||||||
|
|
||||||
|
Chaîne : `panel (JWT) → route def → sudo -n secubox-<module>ctl <verbe> --json →`
|
||||||
|
le `ctl` (root) valide, agit, audite, renvoie un payload `--json` que le panel rend.
|
||||||
|
Le grant sudoers (`/etc/sudoers.d/secubox-<module>`, `0440`, commande exacte, sans
|
||||||
|
wildcard) est **livré par le paquet** et documenté. Symptôme si absent :
|
||||||
|
`PermissionError` → 500 (« request error » / panneau vide).
|
||||||
|
|
||||||
|
Note aggregator : une route servie in-process par l'aggregator n'apparaît qu'après
|
||||||
|
`systemctl restart secubox-aggregator` (le code module est importé au démarrage).
|
||||||
|
|
||||||
|
Réf. d'implémentation : `secubox-cvectl` (génération de règles WAF),
|
||||||
|
`secubox-profilectl` (bascule on/off des modules). Contrat complet :
|
||||||
|
[`.claude/MODULE-COMPLIANCE.md`](../.claude/MODULE-COMPLIANCE.md) → *Privileged Operations*.
|
||||||
|
|
||||||
### Dual-vhost split — REQUIRED pour les modules avec UI applicative
|
### Dual-vhost split — REQUIRED pour les modules avec UI applicative
|
||||||
|
|
||||||
Un module qui embarque une application avec sa propre interface web
|
Un module qui embarque une application avec sa propre interface web
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user