Compare commits

...

3 Commits

Author SHA1 Message Date
CyberMind
1d7ca0cd22
Merge pull request #602 from CyberMind-FR/feature/601-vm-vm-shows-0-containers-lxc-enumeration
Some checks are pending
License Headers / check (push) Waiting to run
fix(vm): /vm/ shows 0 containers — sudo LXC enumeration + RAM column key
2026-06-15 14:50:46 +02:00
a610ffd276 fix(vm): lxc-ls column key RAM not MEMORY (#601)
lxc-ls -F MEMORY is rejected ('Invalid key') and emits no container rows,
so even with the sudo fix the list was empty. Use RAM (the valid key).
2026-06-15 14:49:19 +02:00
5800ad713d fix(vm): LXC enumeration via sudo so /vm/ shows real containers (closes #601)
The aggregator mounts the vm module in-process as the unprivileged secubox
user, so bare lxc-ls couldn't see root's /var/lib/lxc → 0 containers. Route
LXC read+lifecycle through sudo -n with a new read-only-ish grant
(/etc/sudoers.d/secubox-vm: lxc-ls/info/start/stop, visudo-validated);
lxc-create/destroy stay root-only. postinst reloads secubox-aggregator so
the in-process module refreshes. KVM/libvirt readings were already correct.
2026-06-15 14:46:00 +02:00
5 changed files with 62 additions and 8 deletions

View File

@ -41,6 +41,17 @@ def run_cmd(cmd: list, timeout: int = 60) -> tuple:
return "", str(e), 1 return "", str(e), 1
def run_priv(cmd: list, timeout: int = 60) -> tuple:
"""Run a host privileged command via the read/lifecycle sudo grant.
The aggregator mounts this module in-process as the unprivileged `secubox`
user, which cannot see root's /var/lib/lxc containers — bare `lxc-ls`
returns nothing, so the dashboard reported 0 containers (#601). The grant
in /etc/sudoers.d/secubox-vm covers read + lifecycle only.
"""
return run_cmd(["sudo", "-n"] + cmd, timeout=timeout)
def is_libvirt_running() -> bool: def is_libvirt_running() -> bool:
"""Check if libvirtd is running.""" """Check if libvirtd is running."""
_, _, code = run_cmd(["systemctl", "is-active", "--quiet", "libvirtd"]) _, _, code = run_cmd(["systemctl", "is-active", "--quiet", "libvirtd"])
@ -81,7 +92,9 @@ def get_virsh_vms() -> list:
def get_lxc_containers() -> list: def get_lxc_containers() -> list:
"""List all LXC containers.""" """List all LXC containers."""
containers = [] containers = []
stdout, _, code = run_cmd(["lxc-ls", "-f", "-F", "NAME,STATE,IPV4,MEMORY"]) # NB: the memory column key is `RAM` — `MEMORY` is rejected by lxc-ls
# ("Invalid key") and yields zero output (#601).
stdout, _, code = run_priv(["lxc-ls", "-f", "-F", "NAME,STATE,IPV4,RAM"])
if code != 0: if code != 0:
return containers return containers
@ -122,7 +135,7 @@ def get_lxc_info(name: str) -> dict:
"""Get detailed LXC container info.""" """Get detailed LXC container info."""
info = {"name": name, "type": "lxc"} info = {"name": name, "type": "lxc"}
stdout, _, code = run_cmd(["lxc-info", "-n", name]) stdout, _, code = run_priv(["lxc-info", "-n", name])
if code == 0: if code == 0:
for line in stdout.strip().split('\n'): for line in stdout.strip().split('\n'):
if ':' in line: if ':' in line:
@ -319,7 +332,7 @@ def start_vm(name: str):
if is_lxc_available(): if is_lxc_available():
for c in get_lxc_containers(): for c in get_lxc_containers():
if c["name"] == name: if c["name"] == name:
stdout, stderr, code = run_cmd(["lxc-start", "-n", name]) stdout, stderr, code = run_priv(["lxc-start", "-n", name])
if code != 0: if code != 0:
raise HTTPException(status_code=500, detail=f"Failed to start: {stderr}") raise HTTPException(status_code=500, detail=f"Failed to start: {stderr}")
return {"status": "started", "name": name} return {"status": "started", "name": name}
@ -347,7 +360,7 @@ def stop_vm(name: str, force: bool = False):
cmd = ["lxc-stop", "-n", name] cmd = ["lxc-stop", "-n", name]
if force: if force:
cmd.append("-k") cmd.append("-k")
stdout, stderr, code = run_cmd(cmd) stdout, stderr, code = run_priv(cmd)
if code != 0: if code != 0:
raise HTTPException(status_code=500, detail=f"Failed to stop: {stderr}") raise HTTPException(status_code=500, detail=f"Failed to stop: {stderr}")
return {"status": "stopped", "name": name} return {"status": "stopped", "name": name}
@ -371,8 +384,8 @@ def restart_vm(name: str):
if is_lxc_available(): if is_lxc_available():
for c in get_lxc_containers(): for c in get_lxc_containers():
if c["name"] == name: if c["name"] == name:
run_cmd(["lxc-stop", "-n", name]) run_priv(["lxc-stop", "-n", name])
stdout, stderr, code = run_cmd(["lxc-start", "-n", name]) stdout, stderr, code = run_priv(["lxc-start", "-n", name])
if code != 0: if code != 0:
raise HTTPException(status_code=500, detail=f"Failed to restart: {stderr}") raise HTTPException(status_code=500, detail=f"Failed to restart: {stderr}")
return {"status": "restarted", "name": name} return {"status": "restarted", "name": name}
@ -404,9 +417,11 @@ def delete_vm(name: str):
for c in get_lxc_containers(): for c in get_lxc_containers():
if c["name"] == name: if c["name"] == name:
if c.get('state') == 'running': if c.get('state') == 'running':
run_cmd(["lxc-stop", "-n", name, "-k"]) run_priv(["lxc-stop", "-n", name, "-k"])
stdout, stderr, code = run_cmd(["lxc-destroy", "-n", name]) # lxc-destroy is intentionally NOT in the sudo grant — deleting
# containers from an unauthenticated endpoint stays root-only.
stdout, stderr, code = run_priv(["lxc-destroy", "-n", name])
if code != 0: if code != 0:
raise HTTPException(status_code=500, detail=f"Failed to delete: {stderr}") raise HTTPException(status_code=500, detail=f"Failed to delete: {stderr}")

View File

@ -1,3 +1,16 @@
secubox-vm (1.0.1-1) stable; urgency=medium
* fix(lxc): /vm/ reported 0 containers though the host runs 20 LXC
containers (#601). The aggregator mounts this module in-process as the
unprivileged `secubox` user, so bare `lxc-ls` could not see root's
/var/lib/lxc → empty list. LXC read + lifecycle now go through `sudo -n`
via a new read-only-ish grant (/etc/sudoers.d/secubox-vm: lxc-ls/info/
start/stop, visudo-validated). lxc-create/lxc-destroy stay root-only.
postinst reloads secubox-aggregator so the in-process module refreshes.
(KVM/libvirt readings were already correct: no /dev/kvm, libvirtd off.)
-- Gerald KERMA <devel@cybermind.fr> Mon, 15 Jun 2026 13:00:00 +0200
secubox-vm (1.0.0-1) stable; urgency=low secubox-vm (1.0.0-1) stable; urgency=low
* Initial release * Initial release

View File

@ -5,6 +5,19 @@ case "$1" in
install -d -o root -g root -m 755 /var/lib/secubox/vm install -d -o root -g root -m 755 /var/lib/secubox/vm
install -d -o root -g root -m 755 /var/lib/secubox/vm/iso install -d -o root -g root -m 755 /var/lib/secubox/vm/iso
install -d -o root -g root -m 755 /var/lib/secubox/vm/disks install -d -o root -g root -m 755 /var/lib/secubox/vm/disks
# #601 — validate the read+lifecycle cscli/lxc sudo grant; drop it if
# malformed so a bad drop-in can never break sudo for the whole system.
SUDOERS=/etc/sudoers.d/secubox-vm
if [ -f "$SUDOERS" ]; then
chmod 0440 "$SUDOERS" || true
if command -v visudo >/dev/null 2>&1 && ! visudo -cf "$SUDOERS" >/dev/null 2>&1; then
echo "secubox-vm: invalid sudoers drop-in, removing" >&2
rm -f "$SUDOERS"
fi
fi
# The aggregator mounts this module in-process; reload it so the new code
# + LXC listing take effect (the per-module service is not the live path).
systemctl try-reload-or-restart secubox-aggregator.service 2>/dev/null || true
systemctl daemon-reload systemctl daemon-reload
systemctl enable secubox-vm.service systemctl enable secubox-vm.service
systemctl start secubox-vm.service || true systemctl start secubox-vm.service || true

View File

@ -14,3 +14,7 @@ override_dh_auto_install:
install -m 644 menu.d/903-vm.json $(CURDIR)/debian/secubox-vm/usr/share/secubox/menu.d/ install -m 644 menu.d/903-vm.json $(CURDIR)/debian/secubox-vm/usr/share/secubox/menu.d/
install -d $(CURDIR)/debian/secubox-vm/usr/lib/systemd/system install -d $(CURDIR)/debian/secubox-vm/usr/lib/systemd/system
install -m 644 debian/secubox-vm.service $(CURDIR)/debian/secubox-vm/usr/lib/systemd/system/ install -m 644 debian/secubox-vm.service $(CURDIR)/debian/secubox-vm/usr/lib/systemd/system/
# #601 — read+lifecycle sudo grant so LXC enumeration works under the
# unprivileged `secubox` user the aggregator mounts this module as.
install -d $(CURDIR)/debian/secubox-vm/etc/sudoers.d
install -m 0440 debian/sudoers.d/secubox-vm $(CURDIR)/debian/secubox-vm/etc/sudoers.d/secubox-vm

View File

@ -0,0 +1,9 @@
# SecuBox VM module — host virtualization management as the `secubox` user.
# The aggregator mounts this module in-process as `secubox`, which cannot see
# root's /var/lib/lxc containers without sudo. READ + container LIFECYCLE only.
# lxc-create / lxc-destroy are deliberately NOT granted — creating/deleting
# containers stays root-only (these endpoints carry no JWT). CSPN least-priv.
secubox ALL=(root) NOPASSWD: /usr/bin/lxc-ls *
secubox ALL=(root) NOPASSWD: /usr/bin/lxc-info *
secubox ALL=(root) NOPASSWD: /usr/bin/lxc-start *
secubox ALL=(root) NOPASSWD: /usr/bin/lxc-stop *