87 KiB
Executable File
SysDeck
A drop-in plugin for an existing Cockpit install — twenty-six domain modules behind one dashboard.
Author: Jeremy Anderson · info@dcos.net · https://dcos.net Version: 0.4.1 · License: MIT
What this is
SysDeck is a cockpit-native plugin that consolidates the day-to-day work of a Linux operations team — containers, firewall, integrity auditing, network security, service mesh, encryption vaults, fleet compute, Kata Containers, firmware, image building, mining, theme engine, hardware authentication, DAG-driven build orchestration, system monitoring, hardware sensors, system benchmarking, package management, policy & permissions, database control, Jellyfin media server, photo manager (PhotoPrism/Piwigo/Lychee/Nextcloud-Memories/LibrePhotos), remote filesystem manager (Ceph/GlusterFS/MooseFS/BeeGFS/OrangeFS), a 3rd-party Cockpit module installer (45Drives Navigator/File-Sharing/ZFS-Manager, cockpit-pacman, cockpit-identities, cockpit-sensors, cockpit-benchmark — each pulled on demand with the license, developer, source URL, and homepage shown inline next to a 1-click Install button), and a service/port editor (a first-class sidebar entry that enumerates every listening TCP socket, cross-references against a SERVICES_REGISTRY of 9 known services — ssh, cockpit, caddy, varnish, mariadb, ollama, openwebui, hermes, odysseus — and lets the operator edit the port in each service's config file with an atomic write + systemctl restart) — into a single dashboard accessible from the cockpit web UI.
The plugin ships as static HTML+JS+CSS plus a Python bridge helper package. It installs under /usr/share/cockpit/sysdeck-*/ and is discovered automatically by the cockpit-bridge. No separate web server, no Node.js runtime, no database — the plugin runs inside the cockpit web service.
v0.4.1 highlights (cockpit module detection — 100% console/host parity)
0.4.0 brought the Unix login. 0.4.1 closes the last compatibility gap:
every cockpit module installed on the host is now detected and loaded
into the Next.js console too — distro modules like cockpit-machines
and cockpit-podman, addons, anything with a menu entry in its
/usr/share/cockpit/<pkg>/manifest.json:
- Detection is pure filesystem — the same discovery the cockpit
shell performs.
sysdeck-*modules are skipped (native panels already ship here) and chrome without a menu (base1,shell) never shows. Works with cockpit stopped or absent;SYSDECK_COCKPIT_SCANadds extra scan roots (colon-separated) for staged/DESTDIR trees. With no cockpit tree the surface shows a clearly-badged typical-distro set. - A "Cockpit" sidebar group appears with every detected module —
each opens a detail view: manifest identity, shipped files with
sizes, live backend presence probes (
virsh,podman,nmcli,pkcon...) plus on-demand version probes, and a jump to the native console panel covering the domain (machines/podman → Containers & VMs, packagekit → Packages, networkmanager → Network Security, metrics → Monitoring...). The ⌘K palette searches them too, and the Cockpit Modules hub panel lists everything with LIVE/DEMO provenance. - The UI codenames are retired — no more "web edition" or edition subtitles anywhere in the console; the identity is simply SysDeck with a single clean subtitle: dcos.net (login banner, sidebar, status bar). The page title is "SysDeck".
v0.4.0 highlights (Unix Login Edition)
0.3.1 gated the web console behind one shared password. 0.4.0 replaces it with the login model the whole project is named after: sign in with a Unix account, verified by the host's PAM stack — the same mechanism Cockpit uses at its own login screen. The host decides; the console keeps no password data of its own.
- PAM login —
web/scripts/pam-auth.py, a stdlib-only ctypes client oflibpam, runspam_start→pam_authenticate→pam_acct_mgmtunder thesysdeckservice when/etc/pam.d/sysdeckexists, else the stockloginstack. Credentials travel over stdin, never argv. Ship your own/etc/pam.d/sysdeckto tailor the stack (MFA modules included, if you want them). - User-bound sessions — the
sd_sessioncookie becomesv2.<exp>.<userB64>.<hmac>; the shell shows a cockpit-style account menu (avatar,user@host, PAM/local provenance, the wheel/sudo "Administrative access" badge, a live session-expiry countdown with a draining life bar) and the status bar carriesuser@host. 0.3.1 v1 tokens still verify as legacy sessions — upgrades don't log anybody out. The fester service gates its REST + WS surface on the same v2 token. - Three auth modes —
SYSDECK_AUTH_MODE=pam(default, cockpit faithful: run the service as root so any unix account can sign in),pam+local(PAM first,SdUserscrypt accounts as the fallback for unprivileged installs),local(console accounts only). Local accounts are managed withbun scripts/manage-users.mjs list|add|passwd|disable|enable|remove. - Lockout like sshd — failures rate limited per-IP and per-username (5/min each); wrong-user and wrong-password return the same generic answer; every attempt audited with the unix username as actor. A wedged PAM helper fails CLOSED, never silently falls back.
- The login screen got the fester treatment — host identity banner (hostname + OS, exactly what cockpit leads with), aurora/grid backdrop in the active console theme, caps-lock detection, a one-shot error shake, and the amber default-password nag (local modes) until the seeded account is rotated.
- Cockpit edition untouched — all 29 modules keep working under
cockpit exactly as before; the bridge guards (
check-bridge-subcommands218 calls / 28 modules, manifest consistency) still pass. This revision's changes live inweb/and the docs.
v0.3.1 highlights (login gate for the web edition)
The 0.3.0 audit left one honest gap: the web edition had guards but no login. 0.3.1 closed it the LAN-side way — a cockpit-style shared password (superseded by 0.4.0's Unix-account login):
- one shared password —
SYSDECK_WEB_PASSWORDinweb/.env(defaultsysdeck; the login screen nags in amber until you set your own). Constant-time compare, per-IP failure rate limit (5/min), every attempt audited with the source IP. - HMAC-signed session cookie — HttpOnly, SameSite=Lax, 12h expiry; the signing key is random per install and persists in SQLite, so restarts don't log you out and the fester mini-service verifies the identical token straight from the same DB — the browser's live WebSocket event stream is gated too, not just the REST routes.
- every surface gated — the page server-renders the login screen
until the cookie verifies; all
/api/*routes answer 401 until signed in; a mid-flight expiry reloads to the login screen instead of erroring. Logout button in the shell header. - The posture stays LAN-side: loopback binds remain the outer
boundary,
SYSDECK_SESSION_SECURE=1adds theSecurecookie flag when the console fronts TLS. See QUICKSTART §10.4.
v0.3.0 highlights (AI Gateway Edition)
v0.3.0 integrates klanker-gate — the Frosty Deno LLM gateway (Deno 2 + TypeScript, OpenAI-compatible API, governance, virtual keys, caching, MCP) — as the new AI Gateway module, vendored at /klanker-gate (own independent version 0.9.0, Apache-2.0). klanker-gate is not SysDeck code — it is by TykoDev and is credited in /klanker-gate/ATTRIBUTION.md and THIRD_PARTY.md.
The headline finding of this release: "porting klanker-gate to Arch Linux" required zero upstream source changes. The codebase is Linux-first, not Windows-first (the Windows mentions in the tree are accommodations: reusePortSupported() is linux/darwin-only, the Docker/entrypoint path is POSIX, deno.lock win32 entries are ordinary cross-platform lockfile records). The work was packaging — and it ships:
klanker-gate/arch/— the complete Arch packaging:PKGBUILD(self-packaging,makepkg -si), a hardened systemd unit (StateDirectory,ProtectSystem=full, emptyCapabilityBoundingSet), sysusers/tmpfiles, a/usr/bin/klanker-gaterun wrapper (module-cache warmup +--allow-runscoped to the Deno binary only whenFROSTY_WORKERS>1, mirroring the upstream entrypoint's escalation policy), andINSTALL-ARCH.md(the full runbook: postgres provisioning, env, verification, SysDeck wiring). Bonus: moving to Arch unlocksFROSTY_WORKERSmulti-process serving viaSO_REUSEPORT— impossible on Windows.- cockpit side —
bridge/klanker.py(10 subcommands: status, providers, models, vkeys, logs, analytics, runtime, service, journal, localstack — stdlib REST client againstKLANKER_URL, BearerKLANKER_ADMIN_TOKEN, graceful offline JSON, token never echoed) and the fully-builtplugins/sysdeck-klanker/panel (status card, spend in µUSD→USD, providers/vkeys/recent-requests tables, runtime topology, local stack wiring card, service control + journal viewer). - web side — the hybrid AI Gateway panel (Integrations group): live REST against the gateway when it runs, clearly-badged demo data when it doesn't (this sandbox has no Deno/Postgres); flips to
source: liveautomatically withKLANKER_URLset. - local stack first-class (both editions) — the gateway is not SaaS-only:
ollama/lmstudio/sglare native keyless provider types and llama.cpp (llama-server)/KoboldCpp/vLLM plug in via the genericopenai-compatibletype. New Local stack wiring card live-probes each backend's/v1/modelsfrom the host (klanker localstackbridge subcommand) and shows env + admin-API wiring with copy buttons; the web demo dataset re-seeded local-first (spend/24h ≈ $0.001 — cloud overflow only). See QUICKSTART §10.1. - module toggles (web edition) — every sidebar module can be turned OFF (hidden from the sidebar + ⌘K palette) and back ON from a Disabled section — one click, persisted in SQLite, survives restarts; disabling the active module returns to Overview. Turning the AI Gateway off when you switch stacks is now a hover + click. See QUICKSTART §10.2.
- guards —
check-bridge-subcommandsnow verifies 218 calls across 28 bridge modules (was 216; the audit pass added auth readers+certs); 28 plugin manifests conform. - 0.3.0 security audit — a full-codebase review (bridge helpers, plugin panels, web edition, vendored klanker-gate): bridge write primitives now fail closed (cgroup-set path+control validation, artifacts-clear/build-delete/build-log id validation, profile-create name validation, hwalert's
sudo sh -cremoved, db start/stop/restart registry-gated, db query read-only-guarded, themes set newline-guarded, packages argument-injection-guarded); the 8 oldest panels escape all interpolations and all 27 manifests droppedunsafe-eval; the web edition binds loopback (dev, fester, production start) with bridge body-cap + rate limit; the vendored gateway is audited-but-unmodified with findings inklanker-gate/arch/SECURITY-UPSTREAM.mdand packaging-layer mitigations (systemd unit refuses to start withoutFROSTY_ADMIN_TOKEN). See QUICKSTART §10.3.
Wire it up (either edition):
KLANKER_URL=http://127.0.0.1:8080
KLANKER_ADMIN_TOKEN=<FROSTY_ADMIN_TOKEN> # see klanker-gate/arch/INSTALL-ARCH.md
Quick start (web edition, from an extracted master tarball):
make web-dev # fester service (background, :3010) + web console (:3000)
Two more 0.3.0 additions close the loop between the editions:
- "Run without Cockpit" runbook — the web edition is fully standalone (no cockpit, no Python bridge, no systemd, no root). The complete deployment guide — dev, standalone production build, the two systemd units,
.envreference, reverse proxy +?XTransformPort=websocket gateway, troubleshooting — ships twice, kept in sync: asweb/README.mdin the tarball and as a first-class panel in the web console (system group, right under Overview, copy-buttons on every command block). - the web-edition skin for Cockpit — since 0.3.0 every Cockpit plugin page links
shared/sysdeck-web.cssafter the base stylesheet, porting the Next.js console's midnight/teal design (accent#3fc9b0, soft-tinted badges, 10px radii, tabular numerals) onto the classic panels;sudo make install-brandingadditionally themes the Cockpit shell chrome (sidebar/header/login, PatternFly v4+v5 covered, distrobranding.cssbacked up first). Revert either withmake uninstall-branding/ removing the skin file. See QUICKSTART §12.
v0.2.0 highlights (Master Edition)
v0.2.0 ships as a master tarball — sysdeck-0.2.0-master.tar.bz2 — bundling the cockpit edition (this tree), the new SysDeck Web Edition (web/ — a standalone Next.js console with 28 bridge modules, an Overview landing view, and the previously-orphaned Hardware Alerts panel), and Fester pre-integrated.
Fester remains its own project upstream (independent repository, independent version line — currently 0.2.1). The master tarball vendors a pinned snapshot at web/mini-services/fester so nothing else needs cloning:
- cockpit side —
bridge/fester.pyis now a real REST client of the fester service (FESTER_URL, defaulthttp://127.0.0.1:3010; 11 subcommands: status, metrics, builds, build, nodes, targets, timeline, sessions, start-build, cancel, replay) andplugins/sysdeck-fester/is a full panel — the v0.0.31 systemd-listing stub is gone. - web side — a dedicated Fester sub-app (live DAG, replay sessions, timeline, failure autopsy, cause graph, interactive debugger, metrics) wired via the
/api/festerproxy and a WebSocket event stream. - fixed — the shipped 0.1.3 Makefile had space-indented recipes (GNU make rejected it with
missing separator); v0.2.0 restores tabs, and every target parses.
Quick start (web edition, from an extracted master tarball):
make web-dev # fester service (background, :3010) + web console (:3000)
Or step by step: make fester-start, then cd web && bun install && bun run db:push && bun run dev. Rebuild the master tarball with make master. See web/README.md for details.
v0.1.3 highlights
v0.1.3 fixes two critical bugs and adds the download/manage UI for builds. The operator reported: "profile workstation still doesnt import current system pkgs. it trys to build only 2." Two root causes were identified and fixed.
- Import bug —
from __init__ importfailed silently._detect_host_packages()relied onfrom __init__ import PKG_MANAGERwhich silently failed in the cockpit superuser channel context (different Python path).PKG_MANAGERdefaulted to"unknown", the host query returned an EMPTY list, and the import wrote nothing. The operator saw "tries to build only 2" because the build used the profile's original template packages. - Import fix —
shutil.which()._detect_host_packages()now usesshutil.which()to findpacman/apt-mark/dnfdirectly — no import dependency, works in any execution context. - Build bug —
--includedoesn't load the config. v0.1.2's--includeflag includes a drop-in fragment ON TOP OF the basemkosi.conf— it does NOT replace the base config. If there's nomkosi.confin the cwd, mkosi uses defaults and ignores the--includefile entirely. This is why v0.1.2 still produced builds with only 2 packages. - Build fix — temp work dir with symlink.
build()now creates a temp directory, symlinks the profile file into it asmkosi.conf, and setswork_dirto that temp dir. mkosi findsmkosi.conf(the symlink), follows it, reads the actual profile. Works for ANY profile path regardless of filename or location. Temp dir is cleaned up after the build finishes. New helper:_prepare_mkosi_work_dir(). - New: artifact download. Each artifact in the Artifacts panel now has a ⬇ Download button. Reads the file via
cockpit.spawn(["cat", path])with superuser, creates a Blob, triggers browser download. - New: artifact management. Each artifact has a 🗑 delete button (per-file). Each profile's artifacts card has a 🗑 Clear all button that removes ALL artifacts for that profile (shows file count + bytes freed).
- New: build management. Each build in the Builds table has a 🗑 delete button. Two-step confirm: (1) delete state + log only, or (2) also delete the profile's entire artifacts dir.
- Regression tests. 11 new unit tests in
TestBuilderArtifactManagement(7 tests) andTestBuilderMkosiTempWorkDir(3 tests). Existing tests updated for the newshutil.whichapproach and removal of--include. Total: 254 tests (was 243; +11). - Version sync. Bumped 0.1.2 → 0.1.3 across all 9 release surfaces.
v0.1.2 highlights
v0.1.2 fixes the critical "zero packages" bug. An operator reported: "the builder absolutely does not work yet. it has zero awareness of packages we tell it to add." Two compounding root causes were identified and fixed.
- Root cause 1 — mkosi never read the profile config.
_backend_build_command()for mkosi was["mkosi", "build", "--output", ..., "--output-dir", ...]with NO flag telling mkosi WHERE the profile config file is. mkosi only reads a file literally namedmkosi.conffrom the cwd. For v0.0.x profiles at/etc/mkosi/mkosi.conf.d/<name>.conf, mkosi ran in that dir, found nomkosi.conf(the file is named<name>.conf), and used EMPTY defaults — zero packages, default distro, default everything. The operator'sPackages=setting was never seen by mkosi. - Fix 1 —
--include <profile_path>._backend_build_command()now ALWAYS passes--include <profile_path>on the CLI. This tells mkosi to explicitly load the profile config by path, regardless of its filename or location. CLI--includeoverrides the defaultmkosi.confdiscovery. - Root cause 2 — legacy
Packages=syntax. Profiles created by v0.0.x used the old indentedPackages=syntax (Packages=\n linux\n linux-firmware\n...). mkosi v22+ (Arch ships 25.x) only understands single-line (Packages=linux linux-firmware ...). The old form is silently parsed as a single package name with embedded newlines, which doesn't exist in any repo — so mkosi installs NOTHING. - Fix 2 — auto-migration. New
_migrate_legacy_mkosi_packages()function detects the old indented syntax and rewrites it to single-line IN-PLACE before the build command is constructed.build()calls this automatically on every mkosi build. The migration is logged in both the build state JSON (warningsarray) and the log file header (# MIGRATED: ...). If the file already uses modern syntax, the migration is a no-op. - Regression tests. 4 new unit tests in
TestBuilderBuildPathcover migration (old syntax rewrite, modern no-op, no-section no-op, end-to-end during build). The existingtest_build_success_pathwas extended to verify--includeis on the command line and points at the profile file. - Version sync. Bumped 0.1.1 → 0.1.2 across all 9 release surfaces. Total unit tests now 243 (was 239 in v0.1.1; +4).
v0.1.1 highlights
v0.1.1 fixes a critical output-path safety bug. An operator reported: "this is NOT a safe output path. fix this now." The v0.1.0 release relied on OutputDirectory= in the scaffolded mkosi.conf to route build outputs to /var/lib/sysdeck/builder/artifacts/<name>/. But when building an OLD v0.0.x profile (whose mkosi.conf had no OutputDirectory= setting), mkosi defaulted to writing image.raw into the cwd — which was /etc/mkosi/mkosi.conf.d/, a system config directory owned by root. mkosi then refused to overwrite the existing image.raw, blocking every rebuild.
- Root cause.
_backend_build_command()for mkosi was just["mkosi", "build"]with no CLI output flags. It trusted the profile'smkosi.confto setOutputDirectory=, which doesn't exist on v0.0.x profiles, can be hand-edited to anything, and is ignored by mkosi if the profile is a drop-in fragment mkosi never reads. - Fix.
_backend_build_command()now ALWAYS passes--output,--output-dir, and--forceon the CLI for mkosi builds. CLI flags overridemkosi.conf, so the output path is forced to/var/lib/sysdeck/builder/artifacts/<name>/<name>.rawregardless of what the profile says.--forceoverwrites any existing image so rebuilds don't fail with "Output path exists already." - Safety check.
build()now refuses to proceed if the resolvedoutput_diris not under/var/lib/,/tmp/,/var/tmp/, or the configuredBUILDER_ARTIFACTS_DIR. Blocks/etc/,/usr/,/boot/,/bin/,/sbin/,/lib/,/root/,/home/, etc. Belt-and-suspenders: even if an operator passesoptions.output_dir=/etc/somethingvia the JS bridge, the build is refused beforesubprocess.runis called. - Legacy profile warning.
build()now detects profiles in/etc/mkosi/mkosi.conf.d/(the v0.0.x drop-in layout) and records a warning in both the build state JSON and the log file: "WARNING: profile is in /etc/mkosi/mkosi.conf.d/ (legacy v0.0.x layout). mkosi may silently ignore this drop-in fragment. Migrate to /etc/mkosi/profiles//mkosi.conf for a real profile." - Log improvement. Build log header now includes the resolved
output_dirso the operator can see exactly where the image will land before mkosi starts. - Regression tests. 2 new unit tests in
TestBuilderBuildPathcover the safety check (refuses/etc/) and the legacy-profile warning. The existingtest_build_success_pathwas extended to verify the mkosi command line includes--output,--output-dir, and--force, and that--output-dirpoints at the per-profile artifacts dir. - Version sync. Bumped 0.1.0 → 0.1.1 across all 9 release surfaces. Total unit tests now 239 (was 237 in v0.1.0; +2).
v0.1.0 highlights
v0.1.0 fixes three compounding bugs in the mkosi build path that were silently producing empty 33M images with no kernel, no systemd, no openssh — the operator clicked Build on a freshly-created profile and got back a 33M image.raw containing only iana-etc + filesystem. Plus a new operator feature requested in the same release cycle: "import current os pkg list to profile should be an option".
- Bug 1 — scaffold location.
profile-createwrote/etc/mkosi/mkosi.conf.d/<name>.conf— a drop-in fragment that mkosi only honors when a parent/etc/mkosi/mkosi.confexists to layer it onto. With no parent, mkosi ran with empty defaults. Fix: each profile now lives in its own directory/etc/mkosi/profiles/<name>/mkosi.conf(the only filename mkosi reads automatically from the cwd).MKOSI_DIRSupdated to scan/etc/mkosi/profilesfirst. - Bug 2 —
Packages=syntax._MKOSI_TEMPLATEand_write_packages_mkosiused the indented-continuation form which was the old systemd-mkosi (<=v15) syntax. mkosi v22+ (Arch ships 25.x) expects single-line space-separated:Packages=linux linux-firmware systemd openssh. Fix: template + writer now emit the modern single-line form. The reader accepts both forms so v0.0.x profiles migrate cleanly on first append/replace. - Bug 3 — output routing. mkosi wrote its output to the cwd (
/etc/mkosi/mkosi.conf.d/image.raw) butbuild()only scanned/var/lib/sysdeck/builder/artifacts/<profile>/for artifacts — so every successful build looked like a failure in the panel. Fix:_MKOSI_TEMPLATEnow setsOutputDirectory=to the per-profile artifacts dir so mkosi writes directly there. - New feature —
profile-import-packages. Queries the host's explicitly-installed package set (pacman -Qqeon Arch,apt-mark showmanualon Debian,dnf repoquery --userinstalledon Fedora) and writes it into a profile's package list via the existing_write_packagesdispatch. Defaults to append mode so the profile's baseline (kernel, systemd, openssh) is preserved. Supports--mode=replace,--dry-runfor preview, and--packages=<json>for manual override (useful for importing a list captured on another host). New polkit exec paths forpacman/apt-mark/dnfadded toorg.sysdeck.builder.modify. - Panel UX. Each profile row in the Builder panel now has a "⇩ Import host pkgs" button. Click → dry-run preview →
window.confirmwith package count, source distro, and first 200 packages → append write. Falls back to operator cancel without writing. - Regression tests. 8 new unit tests in
TestBuilderImportHostPackagescover_detect_host_packagesdispatch (pacman path + dedup), the--packagesoverride end-to-end,--dry-runno-write behavior, and the unknown-profile / no-args / bad-mode / COMMANDS-registration error paths. 4 existing tests inTestBuilderPackagesFieldupdated for the new single-linePackages=syntax; 1 new test (test_mkosi_modern_single_line_input_parsed) guards against a regression where the writer emits the new form but the reader only understands the old one. - Version sync. Bumped 0.0.50 → 0.1.0 across all 9 release surfaces. Total unit tests now 237 (was 228 in v0.0.50; +8
TestBuilderImportHostPackages+ 1 newtest_mkosi_modern_single_line_input_parsed).
v0.0.50 highlights
v0.0.50 fixes a NameError: name 're' is not defined that blocked every build() invocation since v0.0.31. An operator reported: "happens right away on build for a new profile i created." The traceback pointed at _new_build_id() line 492: safe_profile = re.sub(r"[^A-Za-z0-9_-]", "_", profile).
- Root cause.
bridge/builder.py's module-level imports wereimport json / os / shutil / subprocess / sys+from pathlib import Path+from typing import Any. Noimport re._new_build_idhas usedre.subsince v0.0.31 (when the full-featured build operations were added), but no test ever exercised thebuild()code path — the unit tests only coveredprofile_create/profile_copy/profile_deleteand the v0.0.49 package-writing helpers. The bug went undetected for 18 releases (v0.0.31 through v0.0.49) until an operator actually clicked Build on a freshly-created profile. - Fix. Added
import reto the module-level imports inbridge/builder.py. Removed the now-redundant localimport reinside_write_packages_vmdb2(it was a v0.0.49 workaround that's no longer needed — the module-level import covers both callers). - Regression tests. 9 new unit tests in
TestBuilderBuildPathcover_new_build_id(format, sanitization of unsafe chars like dots, preservation of safe chars like hyphens/underscores, and an explicit assertion thatreis in the builder module's globals so the bug can't recur if anyone refactors the imports). The class also includesbuild()end-to-end tests with mockedsubprocess.run— success path (verifies state file + log file written, response shape correct, subprocess actually called), unknown profile, no args, backend-not-installed, and non-zero returncode records state "failed". All tests mock the module-levelBUILDER_STATE_DIR/BUILDER_LOGS_DIR/BUILDER_ARTIFACTS_DIRso they run hermetically. - AST audit. Ran an AST-based audit of
bridge/builder.pyto find any other names used at module level but not imported. No real undefined names found — every flagged item was a comprehension local, tuple-unpacking target, except-clause target, or__file__. The build path is now fully exercisable by tests. - Version sync. Bumped 0.0.49 → 0.0.50 across all 9 release surfaces.
v0.0.49 highlights
v0.0.49 closes the loop on the Image Builder profile-creation flow. Per user directive: "we should allow adding a pacman -Sy applist.txt with a literal list of baseline apps for the profile being generated." Previously the operator scaffolded/copied a profile, then had to drop to a shell to edit the package list. Now both the Create Profile and Copy shipped profile forms include an inline package-list field — paste the list or upload applist.txt, pick a merge mode, and the bridge writes the packages to the right place for whichever backend was selected.
- All 4 backends supported. Each writes to its native package-list location: mkosi →
[Packages]section of<name>.conf, vmdb2 →bootstrap.includelist in<name>.yaml, archiso →packages.x86_64in the profile dir, live-build →config/package-lists/sysdeck.list. The per-backend writers are intentionally distinct (no generic "update INI/YAML" abstraction) because each format has its own quirks. - Textarea + file upload. The textarea is the source of truth — one package per line,
#comments allowed. The file upload (applist.txt/.list/.confaccepted) populates the textarea via the browser'sFileReaderAPI so the operator can review/edit the uploaded content before submitting. 1 MB cap on uploaded files. - Operator-chooses merge mode. A dropdown toggle on each form: append (default for Copy — preserves the baseline's existing packages like
linux/base, adds the operator's, deduplicates) or replace (default for Create — overwrites the baseline's package file with the operator's list). The operator chooses per-operation. - New bridge helpers.
_extract_opts(args)splits argv into positional +--key=valueopts soprofile-create/profile-copycan accept the new flags without breaking their existing positional signatures._parse_packages_text(text)parses multiline text into a deduped list (strips full-line + inline comments, blank lines, whitespace; preserves first-occurrence order)._write_packages_mkosi/vmdb2/archiso/live_buildare per-backend writers._write_packages(profile_path, backend, packages_text, mode)is the dispatcher. - Extended
profile_create+profile_copy. Both accept--packages=<json>(JSON-encoded so newlines/quotes survive the argv boundary) and--mode=append|replace. Both return a newpackagesfield in their success response:{count, mode, path}. If package-writing fails, the profile is still created/copied and apackages_errorfield is included (non-fatal). - Updated
shared/bridge.js.profileCreate(name, backend, base, packagesText, mode)andprofileCopy(srcName, newName, backend, packagesText, mode).packagesTextis JSON-encoded viaJSON.stringify(). When omitted/null, the bridge writes no package file (back-compat with v0.0.48 callers). - 28 new unit tests in
TestBuilderPackagesFieldcover_parse_packages_text(6 tests),_extract_opts(4 tests), each per-backend writer (10 tests across 4 backends × 2 modes + edge cases), the dispatcher (3 tests), and end-to-endprofile_create/profile_copywith--packages(5 tests). All use tempdirs; none touch real/etc/paths. - Version sync. Bumped 0.0.48 → 0.0.49 across all 9 release surfaces.
v0.0.48 highlights
v0.0.48 fixes a builder-panel bug that surfaced on hosts with only archiso or only live-build installed (i.e. no mkosi/vmdb2). The v0.0.31 Create Profile dropdown fell back to primary.id when no scaffoldable backend was installed — on an archiso-only Arch host or a live-build-only Debian host, the operator could pick "archiso" or "live-build" from the dropdown, click Create, and get hit with Error: profile-create supports ('mkosi', 'vmdb2'); archiso profiles are not scaffolded (use the shipped ones). That error is by design — archiso and live-build use shipped directory-based profile trees, not single-file specs that can be scaffolded from scratch — but the panel gave the operator no way to act on the "use the shipped ones" hint.
- Fix 1: Create Profile dropdown gating.
renderCreateProfileinplugins/sysdeck-builder/builder.jsno longer falls back toprimary.idwhen nomkosi/vmdb2backend is installed. The dropdown only offers actually-scaffoldable backends. When none is installed, the form renders an inline install hint with the exactpacman/aptcommand instead of a dropdown that would have errored. - Fix 2: new "Copy shipped profile" form. A new
renderCopyProfileform lists every shippedarchisoandlive-buildprofile discovered viaprofiles()(typicallybaselineandrelengfor archiso) and offers a one-click copy into/etc/. Source profiles are grouped by backend in an<optgroup>; the new-name input is free-text. This is the supported way to create profiles for the directory-based backends. - New bridge command:
profile-copy.bridge/builder.pygains aprofile_copy()function (registered in theCOMMANDSdict asprofile-copy). It copies/usr/share/archiso/configs/<src>/→/etc/archiso/configs/<new>/(and the live-build equivalent). Validates the new-name (rejects slashes and./..to prevent path traversal), resolves the source viaprofiles(), refuses non-directory-based backends with a clear "use profile-create" hint, refuses if the destination already exists, and returns structured{copied, backend, source, source_path, name, path}on success. Uses the same polkit action asprofile-create(org.sysdeck.builder.modify) — no new polkit file needed. - New bridge.js method.
bridge.builder.profileCopy(srcName, newName, backend)runs with{ superuser: 'try' }, same asprofileCreate/profileDelete. - Destination-roots refactor.
ARCHISO_COPY_DESTandLIVE_BUILD_COPY_DESTare now module-level constants inbridge/builder.py(was: hardcodedPath("/etc/...")literals insideprofile_copy). This mirrors the existingARCHISO_DIRS/LIVE_BUILD_DIRSpattern and lets unit tests patch them with tempdirs instead of touching real/etc/paths. - 15 new unit tests. A new
TestBuilderProfileCopyclass intests/test_bridge_parsers.pycovers argument validation (no args, one arg, slash in name,./..name), source resolution (not-found, wrong-backend hint filter, mkosi/vmdb2 rejection with "use profile-create" hint), success paths (archiso copy, live-build copy, backend-hint-inferred-when-omitted), and failure modes (dest-already-exists with "use profile-delete" hint, source-path-not-a-directory, permission-error returns polkit hint). All tests usetempfile.mkdtemp()andunittest.mock.patch.object(); none touch real/etc/or/usr/share/paths. - Version sync. Bumped 0.0.47 → 0.0.48 across all 9 release surfaces (Makefile
VERSION+ header comment,bridge/__init__.py__version__,packaging/setup.pyVERSION, PKGBUILDpkgver, RPM specVersion+%changelogentry,debian/changelogentry,compat/compat-manifest.jsonversion+_comment,packaging/sysdeck.metainfo.xml<release>,README.mdVersion line). All 9 surfaces now report v0.0.48.
v0.0.47 highlights
v0.0.47 fixes four logic flaws in the v0.0.46 release. Per user directive: "we need to fix a few logic flaws i do things a certain way on my servers so ill correct the ports on a firewall script or two. the web server template, and vps template i setup the webserver on 8080 and varnish on 80 for an automatic cache environment. we should move the service/ports editor to its own module entry for ease of access. the glances we should default to enabling the built in webui and embedding that into our module instead it visually looks stunning in comparison to ours."
- Firewall: public-webserver.sh port-topology fix. The v0.0.44 template had the cache topology backwards — it exposed Caddy on
:80and Varnish on:8080. v0.0.47 flips it to match the operator's documented cache-environment setup: Varnish is the public cache front on:80, Caddy HTTP backend lives on:8080(loopback only) — Varnish's cache-miss target — and Caddy HTTPS terminates TLS on:443(public). TheVARNISH_PUBLICtoggle is removed entirely::8080is now ALWAYS loopback-only because the previous default (VARNISH_PUBLIC=true) exposed the cache-miss backend path to the internet, letting clients bypass Varnish and hit Caddy directly. Defense-in-depth drops were added for:8080alongside the existing MariaDB + Caddy admin drops, so even a misconfigured0.0.0.0:8080Caddy bind gets dropped at the firewall. The detect output now reflects the corrected cache-front-of-origin topology. - Firewall: vps-webserver.sh default topology. When Varnish is detected at all (installed but stopped, or running on the upstream default
:6081), the template now forcesVARNISH_PORT=80with a log message explaining the override, and flips Caddy HTTP to:8080loopback. Previously this only happened if Varnish was already listening on:80at runtime — meaning the cache-environment topology depended on the operator having manually moved Varnish to:80first. v0.0.47 makes the cache-front-of-origin topology the explicit default the moment Varnish is detected, matching the public-webserver.sh behavior. - NEW PLUGIN: sysdeck-services (order 45). The Service/Port Editor card that lived at the bottom of the Firewall panel since v0.0.44 has been lifted out into its own first-class sidebar entry — Service / Ports at order 45 — for ease of access. The new panel adds a filter box (search by name/id/port/process), a show-only-editable toggle, and a Refresh button. The bridge surface (
bridge.firewall.services/service-info/set-service-port/restart-service) is unchanged; a newbridge.servicesproxy (4 methods:list/info/setPort/restart) was added toshared/bridge.jsso the new panel has a clean API surface. No new bridge helper file was needed — theSERVICES_REGISTRY, atomic-write logic, andCONFIG_BASE_DIRSallowlist remain inbridge/firewall.pyas the single source of truth. The firewall panel keeps a signpost card pointing operators to the new sidebar entry; theservice/port/editorkeywords were removed from the firewall manifest (they belong to the new services plugin now). - Glances: default-on embedded webui. The Glances panel now auto-starts the built-in Glances webserver (
glances -w --bind 127.0.0.1 --port 61208) on mount — no click required. The iframe is now the primary view, sized to fill the viewport (min-height: calc(100vh - 200px)). The legacy SysDeck snapshot cards (CPU/Memory/Swap/Network/Disk/Processes) are moved into a collapsed<details>at the bottom of the page so they don't push the iframe below the fold. The Stop button is retained for explicit shutdown; we don't stop on unmount because keeping the webserver running speeds re-entry. The manifest CSP was updated toframe-src 'self' http://127.0.0.1:61208 http://localhost:61208so the embedded Glances web UI loads without a CSP violation. - Version sync catch-up. The v0.0.46 release bumped PKGBUILD / spec / debian changelog to 0.0.46 but missed
bridge/__init__.pyandpackaging/setup.py(both stayed at 0.0.45). v0.0.47 catches these up to 0.0.47 alongside every other release surface (Makefile VERSION + comment, metainfo, compat-manifest, README). All 9 release surfaces now report v0.0.47.
v0.0.46 highlights
v0.0.46 is a trademark-scrub release. Per user directive: "you cannot say smoothwall and ipfire where merged into our fw script either. you can say logic derived from or influenced by these projects. its really hard holding your hand on legal issues." The v0.0.36 and v0.0.37 release notes, changelogs, code comments, and worklog entries previously claimed we shipped templates called smoothwall.sh and ipfire.sh and "merged" them into sysdeck-fw. That language implied we incorporated code from those trademarked projects. v0.0.45 rewords every such claim to the legally-safe phrasing: the sysdeck-fw backend's logic is derived from / takes influence from Smoothwall Express and IPFire under our own identifier. We never shipped templates called smoothwall or ipfire.
- Trademark scrub. Every file in the repository was audited for problematic phrasings near "smoothwall" or "ipfire". The script
/home/z/my-project/scripts/scrub_v045_trademark.pyperformed systematic find/replace across 10 files:bridge/firewall.py(EXCLUDED_BACKENDS reasons + docstring),firewall/templates/sysdeck-fw.sh(header comment),firewall/templates/cilium.sh(stale backend reference),tests/test_bridge_parsers.py(test class docstrings + comments),plugins/sysdeck-firewall/firewall.js(header comment),plugins/sysdeck-firewall/manifest.json(keywords list — removedsmoothwall+ipfire, addedsysdeck-fw),README.md(v0.0.36 + v0.0.37 highlights),packaging/debian/changelog(v0.0.36 + v0.0.37 entries),packaging/sysdeck.spec(v0.0.36 + v0.0.37 changelog entries),worklog.md(Task 36 + Task 37 entries). - Legally-safe phrasings used. Every reference to Smoothwall Express or IPFire now uses one of: "takes influence from", "logic derived from", "influenced by these projects". The
EXCLUDED_BACKENDSreasons forsmoothwallandipfirenow read: "other projects' trademarks — we took influence from them for sysdeck-fw instead of shipping templates by those names." - No functional changes. This is a wording-only release. No code paths changed, no templates changed, no bridge subcommands changed. All 141 unit tests still pass. The
sysdeck-fwbackend, the 7 firewall templates, and the v0.0.44 service/port editor are unchanged. - Direct-quote preservation. User-directive quotes that mention "smoothwall" or "ipfire" (e.g. the v0.0.36 directive: "or they can select celium, or smoothwall or ipfire or other firewall scripts") are preserved verbatim as the user's own words. Our commentary around them uses the legally-safe phrasings.
v0.0.44 highlights
v0.0.44 adds three public-server firewall variants and a full service/port editor to the firewall module. Per user directive: "another thing the firewall module needs is a few public server variants. like: remote admin enabled ssh and cockpit, server enabled like caddy and varnish 80 and 8080 w mariadb, an ai llm variant for ollama, hermes, openwebui and oddyseus. and lastly a full service/port editor that detects based on running ports and services detected on them. make it as simple as editing the port to change it in a config on the system. auto restart the associated service if it is changed."
- Three new public-server firewall templates. All three implement the standard start/stop/restart/detect/status/check interface and use modern nftables inet family with named sets, rate limiting with dynamic auto-ban, bogon filtering, invalid TCP flag drops, and per-port log prefixes. They appear in the existing Templates card when the
custombackend is active — no new UI surface needed for selection.remote-admin.sh— SSH (22) + Cockpit (9090). Aggressive rate limiting with auto-ban (4/min SSH, 10/min Cockpit). For VPS / cloud hosts where the operator needs remote shell + web admin from anywhere.public-webserver.sh— Caddy (80/443) + Varnish (8080, public by default per the "80 and 8080" directive) + SSH (22). MariaDB (3306) and Caddy admin API (2019) are bound loopback-only with DEFENSE-IN-DEPTH DROP rules — even if the daemon is misconfigured to bind 0.0.0.0, the firewall drops the packet before it reaches the daemon.ai-llm.sh— Ollama (11434) + OpenWebUI (3000) + Hermes (8000) + Odysseus (8001) + SSH (22). For self-hosted AI LLM stacks. All four AI service ports are public per the user directive; the detect output documents the v0.0.43 "never 0.0.0.0" directive and explains why Ollama's default 0.0.0.0 bind is acceptable here (the firewall gates access, not the bind address).
- Service/Port Editor. Four new bridge/firewall.py subcommands (
services,service-info,set-service-port,restart-service) plus a new "Service / Port Editor" card in the firewall panel. The editor runsss -tlnp(or/proc/net/tcpfallback) to enumerate ALL listening TCP ports on the host, cross-references against a static SERVICES_REGISTRY of 9 services (ssh, cockpit, caddy, varnish, mariadb, ollama, openwebui, hermes, odysseus), and renders one row per service with: editable port input, Save & Restart button, Restart-only button, current port from config, default port, listening ports, processes, PIDs, config file path. Unmapped listeners (ports with no matching registry entry) are shown in an expandable block so the operator can spot services the editor doesn't yet know about. Editing a port writes the new value to the config file atomically (tmpfile + fsync + rename) and runssystemctl restarton the service. Adding a new service to the editor is as simple as adding an entry toSERVICES_REGISTRYinbridge/firewall.pywith its config file paths and port-extraction regex — no other code changes. - Hardening. service_id validated against SERVICES_REGISTRY (CVE-2024-2947 — attacker cannot trick the bridge into editing /etc/shadow). Port validated with strict integer regex 1..65535,
re.fullmatchto reject trailing newlines (CVE-2019-15107 — the v0.0.43 validators usedre.matchwhich let "22\n" slip past; v0.0.44 fixes this). Config path resolved withos.path.realpath+ base-dir allowlist (/etc/or/usr/share/sysdeck/— CVE-2022-30708 symlink-escape defense). Port substitution uses a strict per-service regex (NOT freeform sed) so only the port digits are replaced — comments and other content on the line are preserved. systemctl invoked withshell=False, list argv, env scrubbed (CVE-2024-6126). systemctl binary validated against an allowlist (/usr/bin/systemctl,/bin/systemctl,/usr/sbin/systemctl). Atomic write via tmpfile + fsync + rename defeats partial-write corruption. Theorg.sysdeck.firewall.modifypolkit action (shipped since v0.0.17) already authorizes/usr/bin/systemctl— no polkit changes required. - Regression tests. 32 new tests in two new test classes (
TestFirewallV044ServicesEditor+TestFirewallV044PublicServerTemplates). Tests cover: SERVICES_REGISTRY structure,_validate_service_idand_validate_portaccept/reject (including shell-metachar and path-traversal attacks),cmd_servicesJSON shape,cmd_service_info/cmd_set_service_port/cmd_restart_servicevalidation, end-to-end atomic-write test on a temp config file, no-config-file and regex-no-match error paths, three new template files exist + executable + metadata header + standard dispatch interface + no-sudo. Total tests: 109 (v0.0.43) → 141 (v0.0.44).
v0.0.43 highlights
v0.0.43 fixes a hardening lapse from v0.0.40: the Prometheus port fix introduced 4 references to 0.0.0.0:9095 as a listener address — a wildcard bind that would expose Prometheus to every network interface. All 4 are replaced with 127.0.0.1:9095 (loopback only). A new regression test (TestNoWildcardListeners) scans every bridge helper and panel JS for the 0.0.0.0:<port> pattern and fails the build if any are found — enforcing the "never bind 0.0.0.0" rule permanently.
- No 0.0.0.0 listeners. Per user directive: "we need to make sure we never ever set a web listen address to 0.0.0.0, if anything use 127.0.0.1. we already discussed hardening that should have been fresh." The v0.0.40 Prometheus port fix introduced
webListenAddress: "0.0.0.0:9095"in the bridge config display andweb.listen_address: "0.0.0.0:9095"in the install hint — a wildcard bind exposing Prometheus to the LAN/internet. v0.0.43 replaces all 4 references with127.0.0.1:9095. - Regression test.
TestNoWildcardListenersscans everybridge/*.pyandplugins/*/*.jsfor the0.0.0.0:<port>listener pattern and fails the build if any are found. The only allowed uses of0.0.0.0are CIDR bogon blocks in firewall templates (e.g.0.0.0.0/8) and comments documenting upstream defaults. Total tests: 98 → 100. - Audit confirmed. Every other web listener in the suite already uses
127.0.0.1: Glances (--bind 127.0.0.1), Jellyfin (panel uses127.0.0.1even though Jellyfin itself defaults to0.0.0.0), Photos/RemoteFS/Mining (no web listeners — they manage systemd services).
v0.0.40 highlights
v0.0.40 fixes a port conflict bug: Prometheus and Cockpit-ws both default to port 9090. Since Cockpit is already on 9090 on every SysDeck host, the v0.0.39 bridge was hitting Cockpit-ws instead of Prometheus. Prometheus is moved to port 9095.
- Port conflict fix — Prometheus 9090 → 9095. Per user directive: "prometheus and cockpit both use the same port. so we can assume prometheus was moved not cockpit." Cockpit-ws defaults to port 9090. Prometheus also defaults to 9090. The v0.0.39 bridge hardcoded
http://localhost:9090as the Prometheus API URL — on any host where Cockpit is running, the bridge would hit Cockpit-ws instead of Prometheus and get HTML pages instead of JSON API responses. v0.0.40 moves the Prometheus default to port 9095 (familiar 909x range, no conflict with Pushgateway 9091, Alertmanager 9093, or Cockpit 9090). 10 references updated across 6 files:bridge/prometheus.py(PROM_API_URL default + webListenAddress),plugins/sysdeck-monitoring/monitoring.js(iframe src, open-in-new-tab link, status table URL, install hint port, comment),manifest.json(CSPframe-src),prometheus/sysdeck_scrape.yml(self-scrape target),prometheus/sysdeck_grafana_datasources.yml(datasource URL). The install hint now explicitly tells operators to move Prometheus off 9090 viaweb.listen_addressorARGS. - Operator override. Operators who already run Prometheus on a custom port can override via the
PROMETHEUS_API_URLenvironment variable (e.g.PROMETHEUS_API_URL=http://localhost:9096).
v0.0.39 highlights
v0.0.39 adds a shared tabbed Monitoring module (Prometheus + Grafana) and hardens both bridge helpers to v0.0.37 security standards:
- Monitoring module — Prometheus + Grafana. Per user directive: "we have 2 modules left, we can actually have them share a module with tabs similar to the container/vm module. we should add prometheus, and graphana webui modules." New plugin
plugins/sysdeck-monitoring/with two tabs: (1) Prometheus — status card (version, uptime, targets, alerts firing) + iframe of the real Prometheus web UI athttp://127.0.0.1:9090; (2) Grafana — status card (version, dashboards, datasources) + iframe of the real Grafana web UI athttp://127.0.0.1:3000. Each tab has Refresh / Reload Config / Restart buttons. When a service is not installed, the tab shows a distro-specific install hint (Arch / Debian / Fedora). Plugin count 23 → 24. - Bridge hardening. The existing
bridge/prometheus.py(448 lines) andbridge/grafana.py(413 lines) were written before v0.0.36/v0.0.37 hardening. v0.0.39 brings them up to standard:NoRedirectHandleron all HTTP calls (SSRF defense, CVE-2020-35850), 127.0.0.1-only URL check, env scrubbed on every subprocess (CVE-2024-6126), output sanitized (CVE-2022-36446), nosudo(replaced with directsystemctl+ cockpit superuser channel + polkit),check=Falsewith structured error return, reusesfirewall.pysecurity helpers via import. - New bridge.js surfaces.
bridge.prometheus(8 methods) +bridge.grafana(11 methods). Read-only queries do NOT passsuperuser: 'try'; restart/reload DO. - Polkit action. New
org.sysdeck.monitoring.modifyauthorizessystemctlfor Prometheus + Grafana service management. - Config files shipped. The
prometheus/directory (existed since v0.0.31 but was never installed) is now shipped read-only at/usr/share/sysdeck/prometheus/: scrape configs, alert rules, Grafana datasource + dashboard provisioning YAMLs. - Regression tests. 12 new tests for the prometheus + grafana bridge helpers. Total: 90 (v0.0.38) → 102.
v0.0.38 highlights
v0.0.38 makes the Kata panel production-ready by replacing the mock React bundle with a real Python bridge, and adds a polkit action for future mutating kata verbs:
- Kata panel production rewrite. The v0.0.35-v0.0.37 Kata panel shipped a 470KB pre-built React bundle from the upstream cockpit-kata sub-project. That bundle displayed hardcoded mock data: 5 fake sandboxes (
web-frontend-prod,api-gateway-staging, etc.) with synthetic UUIDs andcreatedAt:"2026-07-15..."timestamps, fake per-sandbox metrics (cpuUsagePercent, memoryUsageMB, historyCpu/historyMemory arrays), a fake QCrows bundle catalog, and a fake PXE status (alwaysdnsmasqRunning:true). The only real features were the QCrows kernel-bundle extraction andkata-runtime check. v0.0.38 deletes the React bundle and ships a vanilla-JS panel (plugins/sysdeck-kata/kata.js) backed by a newbridge/kata.pythat calls the real Kata Containers 3.x APIs:kata-monitorHTTP/sandboxes+/agent-url+/metrics?sandbox=<id>for sandbox enumeration and metrics, filesystem probes of/run/vc/sbs/<id>/(Go shim) and/run/kata/<id>/(Rust shim) for sandbox state,kata-runtime version+kata-runtime env --jsonfor version info,kata-runtime check(exit code) for host capability,systemctl is-active dnsmasq+ real/srv/tftp/probes for PXE status, and real filesystem enumeration of/usr/share/sysdeck/kata/qcrows/for the QCrows bundle catalog. When no sandboxes are running, the panel shows the real empty state — not mock data. The bridge applies all v0.0.36 + v0.0.37 security hardening (strict sandbox-ID validation with^[0-9a-f]{64}$, env scrubbing, output sanitization, no-redirect HTTP to kata-monitor for SSRF defense). - Kata 3.x API correctness. Researched the real
kata-runtimeCLI surface for Kata Containers 3.x. Key finding:kata-runtime listandkata-runtime inspectwere removed in 3.x — the bridge does NOT call them. Sandbox enumeration useskata-monitor's/sandboxesendpoint (plain text, one 64-hex-char ID per line — NOT JSON) plus filesystem enumeration.kata-runtime env --jsonreturns structured JSON with Capitalized Go field names (nojson:struct tags) —Runtime,Hypervisor,Host,Version,Semver— the parser handles this correctly.kata-monitor /metricsreturns Prometheus text format (not JSON), parsed viaprometheus_client.parser.text_string_to_metric_familieswhen available. - New bridge helper.
bridge/kata.pywith 8 subcommands:list,inspect,metrics,summary,version,check,pxe-status,qcrows-list. Reuses the v0.0.37 firewall.py security helpers (SCRUBBED_ENV, _sanitize_output, _validate_filename, _resolve_path_under_base) via import — single source of truth for hardening. - New bridge.js surface.
bridge.katawith 8 methods mirroring the subcommands. All read-only (nosuperuser: 'try'). - Polkit action. New
org.sysdeck.kata.modifyaction authorizingkata-runtime,kata-monitor,ctr,crictl,qcrows-export,qcrows-initrd-regen, andsystemctl. Ships now so future mutating verbs (sandbox create/stop/remove, qcrows-export) are authorized when they land. - Manifest relaxed.
plugins/sysdeck-kata/manifest.jsonrequires.cockpitlowered from286to239(matching every other plugin — the React bundle's cockpit-286 requirement no longer applies). CSP simplified to the standard'unsafe-inline' 'unsafe-eval'(the React bundle'sconnect-src http://127.0.0.1:8090exception is gone — the bridge does the HTTP server-side). Keywords extended withkata-monitor,qcrows,pxe,tftp,cloud-hypervisor,firecracker,qemu. - Regression tests. 13 new tests in
TestKataBridgeProductionclass verifying:cmd_listreturns[](not mock 5 sandboxes),cmd_qcrows_listreturns[](not mock catalog),cmd_summaryreturns real state (kata_runtime_installed: false),cmd_pxe_statusreturns real state (dnsmasq_running: false), sandbox-ID validation rejects malicious input (CVE-2024-2947), and a source-code scan verifyingkata.pycontains NONE of the mock markers (web-frontend-prod,kata-sbx-a1b2c3, etc.). Total tests: 78 (v0.0.37) → 91 (v0.0.38).
v0.0.37 highlights
v0.0.37 introduces the unified "SysDeck FW" backend (which takes influence from Smoothwall Express and IPFire for its zone model + source-verified outbound + AirWall isolation) and expands the CVE-derived security hardening to cover commercial web admin UI panels (cPanel, Plesk, CyberPanel, aaPanel, CloudPanel, HestiaCP, VestaCP, Froxlor, InterWorx, BrainyCP, DirectAdmin, CWP):
- Unified SysDeck FW backend. Per user directive: "we cant call smoothwall or ipfire if its a rewrite, so lets unify them into a unified nftables fw template in the drop down we can call it SysDeck FW." The
sysdeck-fwbackend takes influence from Smoothwall Express (RED/ORANGE/GREEN/BLUE color-zone model) and IPFire (source-verified outbound per-zone CIDR, AirWall isolation for BLUE/WiFi toggleable viaAIRWALL=false, flow offload for hardware acceleration, DMZ port-forwarding) under our own identifier. We do not ship templates called "smoothwall" or "ipfire" — those are other projects' trademarks. Config file at/etc/sysdeck/firewall/sysdeck-fw.conf. Smoothwall and IPFire appear inEXCLUDED_BACKENDSwith the reason documented. - Expanded CVE research. Per user directive: "when i say webmin i mean all web admin ui panels cpanel all of them have a history for us to learn from on the security side of things." v0.0.36 covered Webmin, Cockpit, Ajenti, ISPConfig, Virtualmin. v0.0.37 extends the research to cover cPanel/WHM, Plesk, DirectAdmin, CloudPanel, aaPanel, Froxlor, InterWorx, BrainyCP, CyberPanel, HestiaCP, VestaCP, FastPanel, and CWP. 29 additional CVEs reviewed — full table in
docs/SECURITY-HARDENING.md. Key new CVEs: CVE-2026-41940 (cPanel session-file CRLF injection, CVSS 9.8, CISA KEV — attacker injects\r\nuser=root\r\ninto a pre-auth session file, bypassing password + 2FA), CVE-2025-66431 (Plesk domain-creation RCE-as-root — domain names flow into root-run scripts), CVE-2024-51567 (CyberPanel pre-auth 0-click RCE as root, CVSS 10.0, exploited by PSAUX ransomware Oct 2024 —secMiddlewareonly inspects POST; attackers bypass via PUT/OPTIONS), CVE-2025-48702 (aaPanel tar argument injection — subprocess array form does NOT prevent this; filenames like--checkpoint-action=exec=bash shell.shexecute code), CVE-2026-26279 (Froxlor email-validation logic bug — validation disabled for fields declared as email type), CVE-2023-53945 (BrainyCP crontab RCE — users inject commands through the crontab interface), CVE-2023-35885 (CloudPanel auth bypass via insecure file-manager cookie), CVE-2025-100 (CWP/CentOS Web Panel critical RCE, actively exploited). - New validators (7). Each grounded in a specific commercial-panel CVE:
_validate_domain(CVE-2025-66431 Plesk — RFC 1035 strict domain regex, rejects shell metacharacters, path separators,.., leading/trailing hyphens, enforces 253-char max / 63-char label max),_validate_email(CVE-2026-26279 Froxlor —parseaddr+ charset regex + separate shell-metachar reject; defense in depth on top of input validation),_validate_cron_schedule(CVE-2023-53945 BrainyCP — 5-field cron syntax only; the cron command is never user-supplied),_validate_mysql_identifier(CVE-2026-58048 cPanel — MySQL identifier + reserved-word denylist + no embedded backticks),_sanitize_for_file(CVE-2026-41940 cPanel — strips\r\n\0from any value written to a line-oriented file),_decode_then_validate(CVE-2026-29205 cPanel cpdavd — URL-decode + canonicalize + validate; never validate-then-decode),safe_tar_create(CVE-2025-48702 aaPanel + IWX-CVE-2022-8384 InterWorx — tar--null -T -keeps filenames OUT of argv entirely, defeating argument injection that bypasses the v0.0.36--separator defense). - Security-hardening subcommand expanded.
cmd_security_hardeningnow returns 17 applied items (up from 9 in v0.0.36) and 48 CVEs reviewed (up from 19). The panel's Security Card renders the expanded checklist with the new commercial-panel CVE badges. - Backend count: 3.
FIREWALL_BACKENDShas 3 entries:custom,cilium,sysdeck-fw.EXCLUDED_BACKENDShas 7 entries: the original 5 (ufw, fwbuilder, iptables-legacy, iptables-nft, shorewall) plussmoothwallandipfire(both excluded because they are other projects' trademarks; we took influence from them for sysdeck-fw). - Regression tests expanded.
tests/test_bridge_parsers.pygrows from 45 tests (v0.0.36) to 70 tests (v0.0.37) — 25 new tests for the v0.0.37 validators, each mapped to a specific commercial-panel CVE.
v0.0.36 highlights
v0.0.36 adds a firewall backend dropdown to the Firewall panel and hardens the entire firewall bridge against CVE disclosures found in Webmin, Cockpit, Ajenti, ISPConfig, and Virtualmin:
- Firewall backend dropdown. Per user directive: "next we will add cilium support as a drop down option in the fw area, the user can select custom which is default with the templates that are basic. or they can select celium, or smoothwall or ipfire or other firewall scripts that install cleanly with value for ebpf era and nftables. iptables is old now." Three backends ship:
custom(default — the existing vps-webserver.sh + no-services.sh nftables templates),cilium(Cilium eBPF datapath — replaces nftables as the datapath; identity-based policy via CiliumIdentity labels; L7 policy via Envoy), andsysdeck-fw(unified nftables zone firewall — takes influence from Smoothwall Express and IPFire under our own identifier; we do not ship templates called "smoothwall" or "ipfire" because those are other projects' trademarks). Excluded backends — UFW, fwbuilder, iptables-legacy, iptables-nft, Shorewall, Smoothwall Express, IPFire — are documented in the panel's expandable "Excluded backends" block with the reason for each. - New templates. Two new firewall templates ship under
firewall/templates/:cilium.sh(Cilium eBPF policy loader — applies the default policy at/usr/share/sysdeck/firewall/policies/cilium-default.yaml) andsysdeck-fw.sh(unified nftables zone firewall — RED/ORANGE/GREEN/BLUE zone matrix, source-verified outbound, AirWall isolation for BLUE, optional flow offload, DMZ port-forwarding; takes influence from Smoothwall Express + IPFire under our own identifier). Both implement the standard start/stop/restart/detect/status/check interface. - Security hardening. Per user directive: "now theres inherintly alot of lessons to learn from all the other webmins that came before us. search the web for vuln disclosures for older webmins that we could learn to secure our code from the release info." v0.0.36 hardens the firewall bridge against every CVE disclosure found in Webmin, Cockpit, Ajenti, ISPConfig, and Virtualmin. Full CVE table + hardening checklist in
docs/SECURITY-HARDENING.md. Highlights: CVE-2019-15107 (strict allowlist regex on user input before argv), CVE-2024-2947 (filename validation^[A-Za-z0-9._-]+$), CVE-2026-4631 (--separator before user positionals), CVE-2024-6126 (env scrubbed on every privileged subprocess — LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH, BASH_ENV, ENV, PERL5OPT all dropped), CVE-2022-36446 (all bridge output escaped in JS, never innerHTML), CVE-2022-30708 (path resolution with realpath + startswith base check), CVE-2019-15642 (no eval / pickle / yaml.unsafe_load), CVE-2022-0824 (per-verb polkit check, no UI-trust), CVE-2020-35606 (reject on first mismatch, no sanitization), 2019 Webmin backdoor (release-gate runsgit status --porcelain; reproducible builds with pinnedLC_ALL=C,SOURCE_DATE_EPOCH). - New bridge subcommands (11).
backends,backend-info,active-backend,switch-backend,install-backend,cilium-status,cilium-endpoints,cilium-policy,cilium-policy-apply,cilium-policy-validate,security-hardening. The bridge.js firewall surface exposes 11 new methods mirroring them. - Polkit policy extended.
org.sysdeck.firewall.modifyaction now authorizes/usr/bin/cilium,/usr/sbin/cilium,/usr/bin/cilium-agent,/usr/sbin/cilium-agent,/usr/bin/helm,/usr/sbin/helm(in addition to the v0.0.17 set: nft, iptables, ip6tables). - Regression tests.
tests/test_bridge_parsers.pygrows from 9 tests (v0.0.35) to 45 tests (v0.0.36) — 36 new hardening / backend / Cilium / security-hardening tests, each mapped to a specific CVE. - Plugin count unchanged at 23. No new sidebar entries; this is a feature release for the existing Firewall panel.
v0.0.35 highlights
v0.0.35 restores SysDeck Kata as a standalone sidebar entry and adds three new modules per user directive — Jellyfin media server, photo manager, and remote filesystem manager:
- Kata split. Per user directive: "kata containers should be called SysDeck Kata and moved out of the tools area. and dont call it hidden thats akward." The v0.0.34 layout had Kata Containers demoted to a hidden "tools" entry inside the merged Containers & VMs panel — labeled "Kata Containers (hidden helper)" with priority -1, in
plugins/sysdeck-containers-kata/. v0.0.35 splits Kata back out: renamed to SysDeck Kata, moved toplugins/sysdeck-kata/, converted from atoolsmanifest entry to amenuentry (label "SysDeck Kata", order 27), removed the "hidden helper" wording, dropped the priority -1, and restored a dedicated keywords list. The Containers panel now manages Podman only — the Kata tab and its iframe were removed. The pre-built cockpit-kata React bundle (index.js+index.css) is shipped unchanged. - Jellyfin media server module. Per user directive: "next we will integrate a jellyfin management module where it starts, stops, and loads the admin panel in the module." New plugin
plugins/sysdeck-jellyfin/+ new bridge helperbridge/jellyfin.py. The bridge runssystemctl start/stop/restart jellyfin.servicevia the cockpit superuser channel (polkitorg.sysdeck.jellyfin.modify); the panel iframes the running Jellyfin admin UI athttp://127.0.0.1:8096— same pattern as the v0.0.34 Glances integration. Library list is best-effort viaGET /Library/VirtualFolderson the local Jellyfin instance. - Photo manager module. Per user directive: "as well as a photo manager of equal quality. with its own module." New plugin
plugins/sysdeck-photos/+ new bridge helperbridge/photos.py. Multi-backend design (same shape as the DB Control module): PhotoPrism (port 2342, MIT), Piwigo (port 80, GPL-2.0), Lychee (port 80, MIT), Nextcloud-Memories (port 80, AGPL-3.0), LibrePhotos (port 3000, MIT). Each backend is auto-detected; the bridge runssystemctl start/stop/restart <service>and the panel iframes its admin UI when running. Polkit action:org.sysdeck.photos.modify. - Remote FS manager module. Per user directive: "then a remote fs manager such as ceph, and others but not nfs or amanada fs." New plugin
plugins/sysdeck-remotefs/+ new bridge helperbridge/remotefs.py. Multi-backend: Ceph (LGPL-2.1), GlusterFS (GPL-2.0), MooseFS (GPL-2.0), BeeGFS (BeeGFS EULA — free), OrangeFS (BSD-3). Each backend is auto-detected; the bridge runssystemctl start/stop/restart <service>and the cluster-info subcommand queries backend-specific cluster status (ceph status --format=json,gluster pool list,moosefs-cli info,beegfs-ctl --listnodes,pvfs2-server -m). Polkit actionorg.sysdeck.remotefs.modifyauthorizes the systemctl binary plus ceph / gluster / moosefs-cli / beegfs-ctl / pvfs2-server CLIs. NFS and Amanda are explicitly EXCLUDED per directive — documented in the panel footer and inbridge/remotefs.py:EXCLUDED. - Plugin count 20 → 23. The v0.0.34 hidden helper (
sysdeck-containers-kata) is renamed tosysdeck-kataand promoted to a visible sidebar entry; three new visible modules are added.tests/check_manifest_consistency.pyexpected count updated to 23.scripts/generate-plugins.pyupdated to back up + restore hand-maintained plugins (sysdeck-kataships a pre-built React bundle that can't be regenerated by the suite generator).
v0.0.34 highlights
v0.0.34 consolidates Containers + Kata into one module, integrates the Glances built-in web UI, and expands Themes + Mining to "1999 power-tool style" per user directive:
- Containers + Kata consolidation. Per user directive: "for the containers and kata containers will be merged into one module and replaced by this upload, i will merge this sub project into sysdeck directly and close the other project after this." The standalone
sysdeck-kataplugin is removed; the Kata portion of the merged panel loads the pre-built cockpit-kata React app via iframe to a hidden helper plugin atsysdeck-containers-kata/. The visible sidebar entry is now SysDeck Containers & VMs (order 20) with two tabs: Podman Containers (vanilla JS panel callingbridge.containers) and Kata Sandboxes (iframe to the React app). The standalone cockpit-kata sub-project closes after this release. - Glances web UI integration. Per user directive: "glances is not integrated yet i just assumed you would integrate the built in webui as a module." The bridge now ships
start-web / stop-web / web-statussubcommands that runglances -w --bind 127.0.0.1 --port 61208as a background process; the panel iframes the running web UI athttp://127.0.0.1:61208. The full Glances web UI (every chart, every sensor, every top process, every history graph) is available without SysDeck re-implementing any of it. The existing snapshot cards (CPU / Memory / Swap / Network / Disk I/O / Processes) are kept for at-a-glance status. - Themes 1999 power-tool expansion. Per user directive: "themes and mining they need to be expanded for maximum ui control. think 1999 power tool style here." The new
bridge/themes.pysurfaces:read-config / write-config / get / set / unset / reset / preset-list / preset-apply / variable-list / variable-get / variable-set / variable-reset. Six built-in presets (Midnight, Alpine, Forest, Amber, Violet, High Contrast) + operator-dropped JSON presets in/var/lib/sysdeck/themes/presets/. Twelve CSS variables (--sysdeck-bg,--sysdeck-fg,--sysdeck-accent, etc.) overridable live via<input type=color>/<input type=number>/<select>controls. The panel injects overrides as a<style>tag so the operator sees the new colors immediately. - Mining 1999 power-tool expansion. The bridge now surfaces
summary / threads / pool-config-get / pool-config-set / threads-config-get / threads-config-set / algorithm-get / algorithm-set / pause / resume / pause-worker / resume-worker / start / stop / restart / service-status— every XMRig REST API knob. The panel renders: summary stats (hashrate/pool/uptime), service controls (start/stop/restartxmrig.service), all-workers pause/resume, per-thread hashrate table with per-worker pause/resume buttons, pool config form, thread count form, algorithm picker with 7 RandomX variants.
v0.0.33 highlights
v0.0.33 expands the Policy & Permissions module with the rest of the modern Linux LSM stack, applies a MoE (Mixture-of-Experts) QA pass across the codebase, and rewrites the project documentation:
- Policy module — LSM expansion. Per user directive: "lets now add smack, tomoyo, yama and others as well to the same policy module." Added Smack, TOMOYO, Yama, LoadPin, Lockdown, BPF-LSM, Landlock, plus file capabilities (setcap/getcap). Each is optional — the bridge auto-detects via
/sys/kernel/security/<lsm>/and the panel renders an enable hint with the kernel cmdline when the LSM is absent. SELinux remains skipped (native to the host distro). Thelsm-statussubcommand reads/sys/kernel/security/lsmand renders the active stack as a badge row in the panel header. The polkitorg.sysdeck.policy.modifyaction now authorizes 30+ binaries across ACLs / cgroups / VLANs / eBPF / filecaps / AppArmor / Smack / TOMOYO. - MoE QA pass. A senior QA analyst, senior Linux engineer, senior architect, senior admin, and project-manager-in-devops pass replaced nested ifs with lookup tables (
LSM_PROBES,NON_LSM_CONCERNS,SMACK_FILE_MAP,TOMOYO_FILES,YAMA_SCOPE_NAMES), shiftedfor/whileloops towardmap/filter/reducewhere the data shape allowed it, and kept PEP 868 (typed Python), POSIX (one function = one job, compose with pipes), SEI CERT (noeval, noFunction, all spawn calls use the array form), and MISRA (limited cyclomatic complexity, single exit where practical) in mind. Step-down logic: when a fork of choices appeared, the option that composed best with the rest of the system won. - Documentation rewrite. README, QUICKSTART, BLOG, and LICENSE rewritten with decisive language — no "restored / brought back / surviving artifact" wording. Every design choice is documented as a decision.
- Polkit policy.
org.sysdeck.policy.modifyextended to authorizesmackload,smackcipsos,tomoyo-setprofile,tomoyo-set-profile,tomoyo-savepolicy,tomoyo-init,setcap,getcap(in addition to the v0.0.32 set:setfacl,getfacl,mkdir,mount,ip,bpftool,lsns,aa-enforce,aa-complain,aa-status).
v0.0.32 highlights
v0.0.32 adds two modules — Policy & Permissions and DB Control — and brings the plugin count from 18 to 20:
- Policy & Permissions module.
cockpit-policy— modern policy management and permissions manager for groups. Surfaces five concerns: POSIX ACLs (getfacl/setfacl), cgroups v2 unified hierarchy (mkdir / move PID / write control files), VLANs (ip link add/del type vlan), eBPF programs and maps (bpftool, plus pin-to-bpffs), and namespaces (lsns). AppArmor is optional — the bridge auto-detects whether it is compiled into the kernel; if absent, the panel renders an install hint instead of an empty table. SELinux is intentionally skipped (native to the host distro). Bridge helper:bridge/policy.py. Polkit action:org.sysdeck.policy.modify. - DB Control module.
cockpit-db— unified control for SQL/NoSQL/vector/AI database engines. The bridge helperbridge/db.pysurfaces 32+ engines across SQL/NoSQL/Vector/TimeSeries/Graph/Embedded/Cloud/AI families with summary/status/start/stop/restart/connections/query subcommands. The plugin panel renders per-family engine tables with Start/Stop/Restart buttons, a SQL query runner, and a connections viewer. All mutating operations run via the cockpit superuser channel (polkitorg.sysdeck.db.modify) — nosudoshell-out from JS.
v0.0.31 highlights
- Firewall module — monitor → manager. Template selector, Apply/Stop/Restart, ban/unban IP, clear bans, live service detection. Two templates ship under
/usr/share/sysdeck/firewall/templates/(vps-webserver.sh,no-services.sh); operators can drop more in. - Packages module — sudo → cockpit way.
update-all / install / remove / updatenow actually run the package manager via subprocess; the JS panel passes{ superuser: 'try' }so polkit prompts the operator. Live output renders in an in-panel<pre>— no morealert("Run this command with superuser privileges."). - Builder module — viewer → full-featured.
build / profile-create / profile-delete / build-status / build-log / artifactssubcommands. Builds stream stdout+stderr to/var/lib/sysdeck/builder/logs/<build-id>.log; state lives in/var/lib/sysdeck/builder/state/<build-id>.json; artifacts under/var/lib/sysdeck/builder/artifacts/<profile>/. - Fester rename. Build orch panel menu label and panel title changed to "SysDeck Fester" per user directive.
Two deployment shapes
| Shape | Use case | Lives at |
|---|---|---|
| Cockpit plugin (default) | Drop into an existing cockpit install; access via https://<host>:9090 |
/usr/share/cockpit/sysdeck-*/ |
| Tarball source | Build from source, customize, or contribute | sysdeck-<version>/ source tree |
The cockpit plugin is the primary deliverable.
Module catalog
| # | Module | Codename | Priority | Backend |
|---|---|---|---|---|
| 1 | Containers (Podman) | cockpit-containers |
P0 | podman ps |
| 2 | Firewall Control | cockpit-firewall |
P0 | nft list ruleset + template apply |
| 3 | Integrity Auditor | cockpit-integrity |
P0 | lynis audit system |
| 4 | Network SOC | cockpit-netsec |
P1 | ss -tulpn |
| 5 | Service Mesh | cockpit-mesh |
P1 | kubectl get svc |
| 6 | Encryption Vault | cockpit-vault |
P1 | lsblk -J |
| 7 | Fleet Compute | cockpit-fleet |
P1 | uptime, cockpit peers |
| 8 | SysDeck Kata | cockpit-kata |
P0 | kata-runtime (pre-built React app) |
| 9 | SysDeck Fester (build orchestration) | cockpit-fester |
P1 | systemctl list-units |
| 10 | Firmware Control | cockpit-firmware |
P2 | fwupdmgr, tpm2_pcrread |
| 11 | Image Builder | cockpit-builder |
P2 | mkosi (Arch) / vmdb2 (Debian) |
| 12 | Mining Dashboard (XMRig power tool) | cockpit-mining |
P2 | XMRig REST API + service control |
| 13 | Theme Engine (1999 power tool) | cockpit-themes |
P2 | /etc/cockpit/cockpit.conf + CSS variable surface + 6 presets |
| 14 | Hardware Auth | cockpit-auth |
P2 | pkcs11-tool, pcsc_scan |
| 15 | System Monitor (Glances web UI) | cockpit-glances |
P1 | glances -w (iframe) + snapshot cards |
| 16 | Hardware Sensors | cockpit-sensors |
P1 | sensors (lm_sensors) |
| 17 | System Benchmark | cockpit-benchmark |
P2 | sysbench |
| 18 | Package Manager | cockpit-packages |
P1 | pacman / dnf / apt |
| 19 | Policy & Permissions | cockpit-policy |
P1 | ACLs · cgroups v2 · VLANs · eBPF · namespaces · filecaps · LSM stack (AppArmor/Smack/TOMOYO/Yama/LoadPin/Lockdown/BPF-LSM/Landlock) |
| 20 | DB Control | cockpit-db |
P1 | DB engine CLIs (SQL/NoSQL/vector/AI) |
| 21 | Jellyfin Media Server | cockpit-jellyfin |
P1 | systemctl start/stop/restart jellyfin.service + admin UI iframe (port 8096) |
| 22 | Photo Manager | cockpit-photos |
P1 | Multi-backend: PhotoPrism / Piwigo / Lychee / Nextcloud-Memories / LibrePhotos — start/stop + admin UI iframe |
| 23 | Remote FS Manager | cockpit-remotefs |
P1 | Ceph / GlusterFS / MooseFS / BeeGFS / OrangeFS — start/stop + cluster-info (NFS & Amanda excluded per directive) |
| 24 | Prometheus | cockpit-prometheus |
P1 | Prometheus pushgateway |
| 25 | Grafana | cockpit-grafana |
P1 | Grafana API |
Each module fails closed when its backend tool is absent — the panel shows an install hint instead of crashing.
Architecture
sysdeck-0.0.35/
├── Makefile # install / uninstall / check / dist / distcheck
├── manifest.json # not present (multi-plugin layout — see plugins/)
├── plugins/ # 23 standalone Cockpit plugins
│ ├── sysdeck-containers/ # v0.0.35: Podman only (Kata split out)
│ ├── sysdeck-firewall/
│ ├── sysdeck-integrity/
│ ├── sysdeck-netsec/
│ ├── sysdeck-mesh/
│ ├── sysdeck-vault/
│ ├── sysdeck-fleet/
│ ├── sysdeck-kata/ # v0.0.35: restored to standalone sidebar entry — pre-built cockpit-kata React app
│ ├── sysdeck-fester/
│ ├── sysdeck-firmware/
│ ├── sysdeck-builder/
│ ├── sysdeck-mining/
│ ├── sysdeck-themes/
│ ├── sysdeck-auth/
│ ├── sysdeck-glances/
│ ├── sysdeck-sensors/
│ ├── sysdeck-benchmark/
│ ├── sysdeck-packages/
│ ├── sysdeck-policy/ # Policy & Permissions module
│ ├── sysdeck-db/ # DB Control module
│ ├── sysdeck-jellyfin/ # v0.0.35: Jellyfin media server — start/stop + admin UI iframe
│ ├── sysdeck-photos/ # v0.0.35: Photo Manager — multi-backend start/stop + admin UI iframe
│ └── sysdeck-remotefs/ # v0.0.35: Remote FS Manager — Ceph/GlusterFS/MooseFS/BeeGFS/OrangeFS
├── shared/ # shared bridge.js + sysdeck.css + manifest.json
├── bridge/ # Python bridge helpers (called via cockpit.spawn)
│ ├── __init__.py # package init + distro detection
│ ├── containers.py # podman + systemd aggregation
│ ├── firewall.py # nft ruleset parser + template manager
│ ├── integrity.py # lynis audit runner
│ ├── firmware.py # fwupd + TPM PCR aggregation
│ ├── netsec.py # ss + nft counters aggregation
│ ├── fleet.py # local host + peer-hosts aggregation
│ ├── auth.py # pkcs11-tool + lsusb + pcscd state
│ ├── glances.py # v0.0.34: snapshot + start-web/stop-web
│ ├── sensors.py # lm_sensors normalization + alert thresholds
│ ├── benchmark.py # sysbench result parsing + baselines
│ ├── packages.py # pacman/dnf/apt unified package ops
│ ├── mining.py # v0.0.34: XMRig REST API power tool
│ ├── themes.py # v0.0.34: cockpit.conf + CSS variable surface
│ ├── policy.py # Policy & Permissions module (LSM stack)
│ ├── db.py # database engine control (32+ engines)
│ ├── jellyfin.py # v0.0.35: Jellyfin media server service control
│ ├── photos.py # v0.0.35: photo backend service control (5 backends)
│ ├── remotefs.py # v0.0.35: remote FS backend service control (5 backends, NFS/Amanda excluded)
│ ├── prometheus.py # Prometheus pushgateway log pipeline
│ ├── grafana.py # Grafana dashboard API
│ └── hwalert.py # hardware alert aggregation
├── firewall/ # v0.0.31 firewall templates
│ └── templates/
│ ├── vps-webserver.sh
│ └── no-services.sh
├── packaging/ # RPM spec + PKGBUILD + debian/ + setup.py + polkit/
├── compat/ # compat-manifest.json (per-distro dep matrix)
├── tests/ # unit tests + build-time guards
├── scripts/ # generate-plugins.py
├── prometheus/ # prometheus configs + grafana dashboards
├── standalone-plugins/ # external cockpit plugin sidebar registrations
├── docs/ # INSTALL.md
├── README.md
├── QUICKSTART.md
├── BLOG.md # release narrative
├── QA.md # QA notes per release
├── THIRD_PARTY.md # third-party attributions
├── LICENSE # MIT
├── worklog.md # per-task development log
├── sysdeck-diagnose.sh # diagnostic script (install issues)
└── cockpit-smoke-test.sh # smoke-test for cockpit itself
Bridge layers
The bridge client is the only path to the system. It is layered so each concern can evolve independently:
| Layer | Responsibility | Module entry-point |
|---|---|---|
| Transport | Raw cockpit.spawn / cockpit.file / cockpit.dbus / cockpit.metrics |
rawSpawn, file, dbus, metricsTap |
| Resilience | Retry with exponential backoff for transient failures | withRetry |
| Pooling | Collapse identical in-flight spawns into one bridge round-trip | pooledSpawn |
| Permission | Gate privileged calls on cockpit.permission state |
spawnPrivileged, permission |
| Per-module helpers | Typed façade per domain (containers, firewall, policy, db, …) | containers, firewall, policy, db, … |
Panels import the per-module helpers and never touch the lower layers directly.
Cross-cutting contracts
- Cockpit manifest. Each plugin's
manifest.jsonregisters it with cockpit under theindexmenu key. Cockpit servesindex.htmlathttps://<host>:9090/cockpit/@localhost/sysdeck-<name>/index.html. - cockpit.js. The global
cockpitobject is loaded via<script src="../base1/cockpit.js">— a path relative to the plugin root that the cockpit-bridge resolves. The sharedbridge.jsaccesses the globalwindow.cockpitdirectly (the v0.0.22import cockpit from "../base1/cockpit.js"pattern was broken becausecockpit.jsis a UMD/IIFE, not an ES module). - Module registry.
scripts/generate-plugins.pyis the single declarative source for the 20-module catalog. Adding a module means appending one entry and dropping a plugin directory — no other wiring. - Python bridge. The
bridge/directory contains standalone CLI scripts invoked by absolute path:python3 /usr/lib/sysdeck/bridge/<module>.py <subcommand> [args]. Nopython3 -mflag, noPYTHONPATHmagic (the v0.0.25-m sysdeck.bridge.<module>pattern was broken because it required a nested Python package layout the install target never produced). - Polkit. Privileged bridge operations run via the cockpit superuser channel: the JS panel passes
{ superuser: 'try' }tocockpit.spawn, and the operator authenticates via polkit. The polkit policy at/usr/share/polkit-1/actions/org.sysdeck.policydefines eight privilege domains:system.manage,firewall.modify,packages.modify,firmware.modify,vault.modify,builder.modify,fester.modify,policy.modify,db.modify. Nosudoshell-out from JS anywhere in the suite — this is the cockpit way.
Quick start
See QUICKSTART.md for the five-minute path. The short version:
tar xjf sysdeck-0.0.33.tar.bz2
cd sysdeck-0.0.33
sudo make install
sudo systemctl restart cockpit.socket
# open https://<host>:9090 → 20 "SysDeck <Name>" entries appear in the sidebar
Coding standards
The codebase follows four reference standards, adapted to TypeScript/JavaScript/Python:
- PEP 868 (spirit). 4-space indentation in Python; 2-space in JS; trailing commas in multi-line literals. Type annotations on every public Python function.
- POSIX. Each function does one thing. Compose with pipes (event bus), not with hidden side effects. No function returns more than one type.
- SEI CERT. No
eval, noFunctionconstructor, no untrusted input reachingspawnwithout an allowlist. Allcockpit.spawncalls use the array form. - MISRA (spirit). Limited cyclomatic complexity per function. Single exit point where practical. No heap allocation in render hot paths.
Refactor discipline
When modifying code, prefer in this order:
- Lookup table — if the construct is a status-to-X mapping, use a
Record<string, X>(JS) ordict/ list-of-tuples (Python). The v0.0.33 policy module usesLSM_PROBES,NON_LSM_CONCERNS,SMACK_FILE_MAP,TOMOYO_FILES, andYAMA_SCOPE_NAMESfor exactly this reason — adding a new LSM is one line in the table, not a new code path. - Functional iterator —
map/filter/reduce/flatMapoverfororwhile. The summary command inbridge/policy.pybuilds the entire capability matrix with two dict comprehensions over the lookup tables. - Early return — flatten nested
ifwith guard clauses. - Switch — only when the case set is closed and a lookup table would be less readable.
When a fork of choices appears, apply step-down logic: pick the option that composes best with the rest of the system (Unix philosophy), document the decision in a comment, and move on.
Comments
Code comments state decisions, not history. Use them to record why a non-obvious choice was made. Avoid "restored", "brought back", "was dropped", "surviving artifact", "previously" — these read as haphazard back-and-forth. Every comment should sound like a decisive decision.
License
MIT — see LICENSE. Third-party attributions: see THIRD_PARTY.md. Author: Jeremy Anderson (info@dcos.net, https://dcos.net).
Release notes
See BLOG.md for the v0.0.33 release narrative and prior-version history.
Project history
See worklog.md for the per-task development log.
Detailed install
See docs/INSTALL.md for RPM, DEB, pip, and manual install paths.