All notable changes to quadletman are documented here. Format follows Keep a Changelog. Versioning follows Semantic Versioning — see docs/ways-of-working.md for the version number scheme and release process.
- RPM package upgrade failed when pip tried to install both the old and new
quadletman wheels simultaneously — during RPM upgrade
%postruns before old package files are removed, so thequadletman-*.whlglob matched two wheels; fixed by using a version-specific glob (quadletman-%{pkg_version}-*.whl) - All 9 volume file-operation API routes (browse, get, save, upload, delete,
mkdir, chmod, archive, restore) operated on the current working directory
instead of returning an error when called on a Podman-managed (quadlet) volume
—
qm_host_pathis empty for quadlet volumes andresolve_safe_path("")resolves to CWD; the UI hid the buttons but the API was unprotected - Archive restore (
volume_restore) always chowned extracted files to the compartment root user, ignoring the volume'sqm_owner_uid— now delegates tovolume_manager.chown_volume_dirwhich respects the helper user - Deleting a quadlet-managed volume only tried to remove a host directory
(which doesn't exist); now correctly removes the
.volumeunit file and reloads systemd sync_helper_usersonly considered container UID maps when deciding which helper users to keep — deleting a container could delete a helper user still needed by a volume withqm_owner_uid; volume owner UIDs are now included- Updating the owner UID of a quadlet-managed volume failed because
update_volume_ownerunconditionally tried tochowna host directory that does not exist for Podman-managed named volumes; thechownis now skipped whenqm_use_quadletis true - Files uploaded, saved, or created in volumes with a custom owner UID were owned
by the compartment root user (
qm-{id}) instead of the volume's configured helper user (qm-{id}-N) —save_file,upload_file, andmkdir_entrynow resolve the correct owner based on the volume'sqm_owner_uid
- Compartment start/stop/restart/resync operations are now queued and executed in the background — the HTTP request returns 202 immediately instead of blocking for 1-60+ seconds; a per-compartment worker drains the queue sequentially; the existing ViewPoller picks up status changes automatically
- Individual container start/stop operations are also queued — previously these blocked the HTTP request and timed out after 30s during image pulls
- Slow container starts (image pulls, heavy init) no longer time out — queued
operations use
lifecycle_operation_timeout(default 10 min) instead of the 30ssubprocess_timeout - Dashboard and compartment detail polling now uses batched
systemctl showandsystemctl statuscalls — one subprocess per type per compartment instead of one per container;_is_unit_enabledfilesystem checks replaced with theUnitFileStateproperty already returned bysystemctl show, eliminating 2 additionalsudo testsubprocess calls per container - Connection monitor and process monitor are now disabled by default for new compartments — existing compartments are not affected (migration 0013)
- Compartment shell bottom sheet now shows a user selector with the compartment
root user and all helper users; selecting a helper user opens a bash shell
running as that user (e.g.
qm-{id}-1000), matching the UID that owns the volume inside the container
- Container fields added in Podman 4.4.0 through 5.7.0 (43 fields) were silently
discarded on create and update — the UI accepted values and validation passed, but
add_containerandupdate_containernever included them in the DB insert/update statements; affected fields includeexpose_host_port,annotation,tmpfs,mount,global_args,group_add,add_host,timezone,memory,pull,pids_limit,shm_size,ulimits,stop_timeout,stop_signal,container_name,run_init,reload_cmd,reload_signal,http_proxy, all SELinux label fields, all startup health check fields, and more - Volume fields added in Podman 4.4.0 through 5.3.0 (12 fields) were similarly
discarded on create —
add_volumewas missinggid,uid,user,image,type,label,volume_name,containers_conf_module,global_args,podman_args, andservice_name
- RPM package installation failed with "The package is not signed" —
publish-repo.shonly signed repository metadata (repomd.xml) but not the individual.rpmfiles; DNF'sgpgcheck=1verifies per-package signatures, not repo metadata - Added
rpm --addsignstep topublish-repo.shso each.rpmcarries an embedded GPG signature before the repository is built - Added
repo_gpgcheck=1to the RPM repo configuration for defense-in-depth verification of both individual packages and repository metadata - Environment file
/etc/quadletman/quadletman.envdocumented in the runbook was never loaded — the systemd unit was missing theEnvironmentFile=directive
- Default
/etc/quadletman/quadletman.envwith commented-out defaults is now shipped in both RPM and DEB packages; marked as a config file so user edits survive upgrades
- Light theme with full semantic CSS theming support
- Per-user theme preference (Dark / Light / System) persisted in database,
selectable from the session modal; System mode follows the OS preference
via
prefers-color-schememedia query QUADLETMAN_PODMAN_VERSION_OVERRIDEsetting to simulate a different Podman version for UI testing (--podman-version=X.Y.Zinrun_dev.sh)- Jinja2 macros:
section_card(card with header + list + empty state),delete_btn(hx-delete confirmation button),empty_state(placeholder)
- All visual styling migrated from inline Tailwind utilities to semantic
qm-*CSS classes inapp.css— templates, macros, and JS files no longer contain raw color, font, or typography Tailwind tokens; the entire UI is now themeable by editing a single CSS file - Modal structure unified: all modals follow the same header / scrollable
content / fixed footer pattern with
qm-modal-body,qm-modal-scroll,qm-modal-footerclasses;display: contentswrapper (qm-modal-data) keeps Alpine scope without breaking flex layout app.cssreorganized into 11 semantic sections (design tokens, page shell, modals, cards, buttons, forms, badges, tabs, tables, metrics, domain components); duplicate and near-duplicate classes consolidated- Container and volume form routes no longer shell out to
podman infofor driver lists — uses startup-cached globals instead, reducing form load time by 0.5-3 seconds run_dev.shnow compiles Tailwind CSS before syncing and detects/stops existing dev instances on startup- JS files (
polling.js,logs.js,modals.js,app.js,navigation.js) migrated from raw Tailwind to semantic classes for full light-theme support - Pod section in compartment detail opens modal directly from card header button instead of requiring a two-step disclosure form flow
- Modal footer buttons scrolled away with content on image unit, network, volume, and artifact forms — footer now stays fixed at the bottom
pair_listmacro called with unsupportedcnkeyword argument in pod formqm-metrics-grid-4class accidentally dropped during CSS consolidation, causing compartment metrics to stack vertically
0.5.1-beta - 2026-03-28
- Fedora 43: PAM authentication failed due to broken
pam_lastlog2.somodule in the defaultloginPAM stack — added a dedicated/etc/pam.d/quadletmanservice config that only loadspam_unix.so - Fedora:
shadowgroup does not exist by default, preventing thequadletmanuser from reading/etc/shadowfor PAM authentication — the RPM%preand DEBpostinstscripts now create the group if missing - Use absolute paths (
/usr/bin/env,/usr/bin/systemctl,/usr/bin/podman,/usr/bin/cat, etc.) in all sudo commands — bare names resolved to/usr/sbin/on Fedora 43 via sudo'ssecure_path, breaking sudoers matching ~/.configdirectory created with root ownership when setting up compartment users — podman and systemd refuse to use a.confignot owned by the user; now creates each intermediate directory with correct ownershipquadletman-agentnot found when running from a venv — now resolves the binary from the same directory as the running Python interpreter- Sudoers file missing entries for read helpers (
cat,test,ls,stat,head,readlink) and interactive terminal (/bin/bash) — volume browser and host shell were broken in non-root mode - Sudoers
(qm-*)RunAs wildcard not supported by sudo 1.9.17 on Fedora 43 — replaced with(%quadletman)group-based matching host.chown()passed-1(no-change sentinel) to shellchownin non-root mode — now resolves to current owner/group viaos.statadmin=Truestdin conflict: when a command both pipes content (secret create, volume import, registry login) and needs sudo password, the password was dropped — now prepends password line before caller's inputDefaultDependenciesplaced in Quadlet[Container]section — Quadlet rejects unknown keys; moved to[Unit]section where systemd expects it
- All compartment commands (systemctl, podman, secrets, metrics) now route
through the authenticated user's sudo (
admin=True) instead of NOPASSWD sudoers entries; sudoers reduced to only PTY terminals, streaming subprocesses, and read-only file access that cannot pipe a password - All
run_in_executor(None, ...)calls in routers replaced withrun_blocking()which propagates ContextVars — required foradmin=Truecredential access in thread pool workers - Use absolute paths (
/usr/bin/systemctl,/usr/bin/podman,/usr/bin/cat, etc.) in all subprocess commands for consistent sudoers matching on Fedora - Dev sudoers (
scripts/sudoers.d/qm-dev) mirrors production - Removed unused
host.run_as_user()(replaced byadmin=Truepath) - Removed
podman quadlet install/rmCLI path — incompatible withadmin=Trueescalation model; unit files now always written directly viahost.write_text
0.5.0-beta - 2026-03-28
- Packages now ship a Python wheel and build the virtualenv at install time — C extension dependencies (pydantic-core, psutil) are compiled against the target system's Python version, fixing "No module named" errors when the installed Python differs from the build host (e.g. Fedora 43 with Python 3.14)
- RPM is now
BuildArch: noarch; DEB is nowArchitecture: all— single package works on any CPU architecture - RPM and DEB build pipelines simplified to one build per format (no per-arch matrix)
- Fedora 43:
SupplementaryGroups=shadow systemd-journalin the systemd unit caused "Failed to determine supplementary groups: No such process" when theshadowgroup does not exist — removed the directive; groups are already assigned viausermodin post-install scripts and picked up by systemd automatically
0.4.4-beta - 2026-03-27
- Per-username login rate limiting (half the per-IP budget) to block distributed credential-stuffing against a single account
- WebSocket connection limiter — max concurrent terminals per client IP
(
QUADLETMAN_WS_MAX_CONNECTIONS_PER_IP, default 10) - WebSocket message size cap (
QUADLETMAN_WS_MAX_MESSAGE_BYTES, default 64 KiB) - Periodic session re-validation on open WebSocket terminals
(
QUADLETMAN_WS_SESSION_RECHECK_INTERVAL, default 60 s) - Terminal open/close audit logging with client IP
SECURITY.mdwith vulnerability reporting instructions
- CSRF timing leak:
secrets.compare_digestnow always runs even when tokens are empty, preventing response-time side-channel - Session cookie missing
path=/— cookie now scoped to the entire application - Session timestamps use
time.monotonic()instead oftime.time()— immune to system clock adjustments - Fernet encryption keys stored in a separate dict from session data — a memory
dump of
_sessionsno longer reveals both key and ciphertext QUADLETMAN_TEST_AUTH_USERblocked whenQUADLETMAN_SECURE_COOKIES=trueto prevent auth bypass in production-like environments
0.4.3-beta - 2026-03-27
- Config file upload UI for Quadlet path fields: environment files, seccomp profiles, containers.conf modules, auth files, decryption keys, ignore files
- Content validation on upload: JSON+key checks for seccomp/auth, TOML for containers.conf
host.pyread helpers (path_isdir,path_isfile,listdir,stat_entry,read_bytes,write_bytes) for non-root privilege escalation
- Non-root mode: all filesystem operations on qm-* paths use
host.*wrappers — volume browser, config file upload/preview, envfile management, unit masking, and quadlet CLI install all work without root run_blocking()propagates ContextVars to executor threads — fixes "admin credentials required" errors in non-root mode
QUADLETMAN_MAX_ENVFILE_BYTES→QUADLETMAN_MAX_CONFIG_FILE_BYTES(old name still accepted)- Envfile routes deprecated in favour of generic configfile routes
0.4.2-beta - 2026-03-27
- 23 configurable
QUADLETMAN_*environment variables for all timeouts, intervals, and limits with minimum-value clamping - Per-compartment locking for all 29 CRUD and lifecycle functions
ServiceCondition/FileWriteFailedexceptions for DB-filesystem atomicity feedback (rolled-back or resync-recommended toasts)_loop_sessioncontext manager for background loop DB sessions with guaranteed rollback- 34 architecture regression tests
- Blocking file I/O in volume routes moved to thread pool executors
- All
subprocess.run()calls now have timeouts - WebSocket PTY cleanup: terminate → wait → kill → wait pattern with hard limit
- SSE generators close source in
finally— no orphaned subprocesses - DB session rollback in background monitoring loops
- DB-filesystem atomicity: rollback DB inserts on unit file write failure
- Error-level log calls corrected from
debugtowarning - Agent API per-request timeout; webhook dedup dict bounded to 10k entries
- Volume routes require
Depends(require_compartment)
- Finnish: Build → Koonti; inline imports moved to top-level across 12 files
- Changelog reordered newest-first
0.4.1-beta - 2026-03-26
- Race condition:
GET /api/compartments/{id}/disk-usagereturned 500 when a compartment was deleted while the request was in flight - Race conditions in background monitoring loops (
metrics_loop,process_monitor_loop,connection_monitor_loop,image_update_monitor_loop,_check_once) — now skip compartments whose Linux user has been deleted - Race condition in
get_metricsandget_metrics_diskroutes when a compartment user is deleted between the UID check and the metrics call - TOCTOU in volume file operations (
volume_get_file,volume_delete_entry,volume_chmod) — replaced check-then-act with try/except metrics.pyfunctions (get_disk_breakdown,get_container_ips,_get_container_pids) now return safe empty defaults when the compartment user is missing instead of crashing withKeyError
- Per-compartment locking for all resource CRUD operations (containers, volumes, networks, pods, images, artifacts, builds, timers, secrets) and lifecycle operations (start, stop, enable, disable, resync) — prevents concurrent mutations on the same compartment
- Lock acquisition timeout (30 s) with
CompartmentBusyexception → HTTP 409 error toast in the UI ServiceConditionbase exception class — service-layer exceptions that must propagate through router catch-all blocks to app-level handlersrequire_compartmentdependency now verifies the Linux user exists (not just the DB record), protecting all routes that use it- User-existence guards on WebSocket routes (
container_terminal,compartment_shell) - Dev server (
run_dev.sh) now recompiles.motranslation files on every start
- Finnish translations: Build → Koonti (noun), rakentaa → koota (verb), rakennettu → koottu (past participle) throughout all UI strings
0.4.0-beta - 2026-03-26
First beta release. All features listed below have been available since the alpha series and are now considered stable enough for testing in non-production environments.
- Compartment-based container management — each compartment gets a dedicated Linux
system user (
qm-{id}) for OS-level process isolation loginctl lingerenabled per compartment so systemd --user units persist after logout and survive reboots- Service templates — snapshot a compartment's full configuration and clone it into new compartments
- Form-based UI for defining containers, pods, images, and networks; quadletman writes the Quadlet unit files
- Full Quadlet key coverage — every container field from the Podman Quadlet spec is exposed, including SELinux labels, health probes, reload commands, pull retries, user namespace mappings, and resource weights
- Pod editing — multi-tab modal form for pods with ports, volumes, DNS, networking, user namespace mappings, and advanced settings
- Image unit editing — modal form for image units with registry auth, platform targeting, tags, retry settings, and advanced options
- OCI artifact units for OCI artifact distribution (Podman 5.7+)
- Named networks with driver, subnet, gateway, IPv6, and DNS settings
- Network mode selection — host, none, slirp4netns, pasta, or named network per container with network aliases
- Build from Containerfile — use a local Containerfile/Dockerfile instead of a registry image (Podman 4.5+)
- AppArmor profile per container (Podman 5.8+)
- Host device passthrough (GPUs, serial ports, etc.)
- OCI runtime selection (crun, runc, kata, custom)
- Init process support (tini as PID 1)
- Log rotation configuration for json-file and k8s-file drivers
- Extra
[Service]directives for advanced systemd configuration
- Managed volumes at
/var/lib/quadletman/volumes/with automatic SELinuxcontainer_file_tcontext - In-browser volume file browser with archive/restore and chmod support
- Helper users for UID mapping — non-root container UIDs map to dedicated host users
- Podman secrets management — create, list, and delete secrets per compartment
- Per-compartment registry login credential storage
- Scheduled timers — systemd
.timerunits withOnCalendar=orOnBootSec= - Timer last-run and next-run status display
- Notification webhooks for
on_start,on_stop,on_failure,on_restart,on_unexpected_process,on_unexpected_connection, andon_image_updateevents with exponential backoff retry
- Live log streaming via SSE
- WebSocket terminal into running containers
- Image management — list, prune dangling, and re-pull images per compartment
- CPU/memory/disk metrics history sampled every 5 minutes
- Per-container restart and failure analytics with timestamps
- Process monitor — records every process under a compartment's Linux user; unknown processes trigger webhooks; regex pattern matching auto-marks known processes
- Connection monitor — records connections by reading
/proc/<pid>/net/tcpfrom each container's network namespace; classifies direction via LISTEN port matching; supports pasta and slirp4netns rootless networking - Host kernel settings (sysctl) management from the UI with persistent configuration
- SELinux boolean management for Podman-relevant booleans
- Database backup download via API
- Export compartments as portable
.quadletsbundle files (Podman 5.8+) - Import
.quadletsbundle files to recreate compartments
- PAM-based HTTP Basic Auth — login with existing Linux OS credentials
- Access restricted to
sudo/wheelgroup members - Kernel keyring credential isolation (via
libkeyutils) with Fernet-encrypted in-memory fallback - CSRF protection (double-submit cookie), HTTPOnly/SameSite=Strict session cookies, and security response headers on every request
- Defense-in-depth input sanitization with branded string types
- Path traversal protection via
resolve_safe_path()andO_NOFOLLOWfile writes
- Version-gated UI — fields and features are shown or hidden based on detected Podman
version using
VersionSpanannotations - Supports Podman 4.4+ through 5.8+; newer features degrade gracefully on older versions
- Runs as a dedicated
quadletmansystem user (backward compatible with root) - Admin operations escalate via the authenticated user's sudo credentials
- Per-user monitoring agents for rootless read-only operations
- Finnish (fi) translation
- Gettext-based i18n framework ready for additional languages
0.3.1-alpha - 2026-03-25
- FIX: release 0.3.0-alpha errors (release pulled)
- ADD: Podman quadlet datatypes alignment
0.3.0-alpha - 2026-03-24
- ADD: Non-root quadletman service user.
- ADD: Removed conntrack dependency and replaced it with proc//net/tcp monitoring instead.
- ADD: Regex grouping to process monitoring.
- ADD: Podman quadlet datatypes alignment
0.2.2-alpha - 2026-03-23
- ADD: Improved internal data model support for Podman version feature gating.
0.2.1-alpha - 2026-03-22
- ADD: Support for unstable releases in distribution.
0.2.0-alpha - 2026-03-21
- ADD: Version gating support by version spans.
- FIX: Package distribution
0.1.1-alpha - 2026-03-20
- FIX: Regression fixes: errors on unsanitized values.
- FIX: Regression fixes: form data handling.
0.1.0-alpha - 2026-03-20
- CHANGE: Migrated to SQLAlchemy 2.0 and Alembic.
- IMPROVE: Use branded strings and adopt stricter security checks.
- ADD: Ubuntu smoke tests
0.0.6-alpha - 2026-03-18
- FEATURE: Web UI over SSH tunnel only.
0.0.5-alpha - 2026-03-18
- Initial version.