django-auth-ldap, python-ldap and pyasn1 move to
conf/requirements-ldap.txt. Installs without LDAP no longer need the
openldap and sasl headers that pip builds python-ldap with; the
installer and the README's system packages drop them, and the README's
LDAP section installs them with that file. The settings already work
without the module.
The Docker image and dev/requirements.txt keep LDAP, so CI still builds
and audits python-ldap.
A theme change compiled Bootswatch with libsass (34 MB, upstream
deprecated) into two tracked files, which updates, rollbacks and new
Docker containers then reset. The 27 themes now ship as
static/css/themes/<theme>.min.css, built by "manage.py build_themes";
the Settings page only saves the theme name and every page links that
file (zephyr when no settings are in context, as on the 500 page).
libsass moves to dev/requirements.txt; a test checks that each shipped
file is its theme compiled from dev/scss. The "Theme SASS Path" setting
is removed, and the theme default "flaty", a typo that never applied,
becomes zephyr, the theme those installs showed. The update and
rollback steps restore an older version's rewritten theme files only
where they are modified.
The compute bootstrap installs gstfsd and webvirt-gstfsd.service in one
helper and no longer installs or restarts supervisor; the README's
compute step does the same, a remote panel's bind goes into a drop-in,
and a compute moving from supervisor removes its gstfsd program.
The installer and the manual installation install four systemd units
(webvirt-cloud, webvirt-novncd, webvirt-socketiod, webvirt-worker) that
run as the web server's user with absolute paths and read
/etc/default/webvirtcloud. gunicorn, socketiod and the worker stop with
KillMode=mixed, so they wind down their own children. supervisor, its
installer code and the contributed systemd files go; the README gets
the update, rollback and a one-time move from supervisor, and the
installer refuses to run over a supervisor install. Docker keeps runit;
novncd's runit script is the one-line form of the others.
eventlet is in maintenance mode. socketiod now runs python-socketio in
threading mode under gunicorn (one gthread worker, which holds the
sessions), with simple-websocket, already a python-engineio dependency.
gunicorn waits 1 s on stop: an open console never finishes, and its
worker ends when the pty closes. The forward loop waits in select
instead of polling, and a stopped worker is reaped by a blocking
waitpid. eventlet leaves the requirements.
The xterm console never connected: socketiod expected a different token
format, its worker had no compute credentials, one global console was
shared by every client and its output broadcast to all of them, and the
Docker image did not run socketiod at all.
- console/tokens.py: one authorize() for noVNC and socketiod; the signed
token is the only credential, checked at connect time.
- socketiod: one pty worker per client (consolecallback with the
instance id), one session per VM, a session cap, input and resize
checked; the parent never opens libvirt. The engine.io same-origin
check is on.
- consolecallback: opens the console with the compute's credentials and
exits with a message when it cannot.
- Docker runs socketiod as a runit service; nginx proxies /socket.io/
with Host and the scheme for the origin check. Supervisor and systemd
start it as well; WEBVIRTCLOUD_SERIAL_CONSOLE=1 enables it.
- Tests: token and session unit tests, socketiod as a process, and a
live test that reads SeaBIOS from a wvc-test VM on the TCP and SSH
hosts.
Static URLs carry no version, but nginx served them with expires max, so
after an update or a theme change browsers kept the old CSS and JS. They
now revalidate each load (304 when unchanged). CI checks the header, the
304 and that a changed file is sent again.
django-qr-code was used for one thing: the QR code in the OTP e-mail,
as an inline SVG and a PNG. It brought pydantic, pydantic-core,
annotated-types and typing-inspection (about 10 MB) and, from 5.0 on,
Pillow. segno, which it used underneath, now draws both images with the
same options (error correction M, 4-module border, 1.8 mm SVG modules,
12 px PNG modules); the SVG, the PNG and the alt text are byte for byte
what django-qr-code 4.2 produced. account.html loaded the tag library
without using it.
django-bootstrap-icons (template tags; it downloads an icon missing from
static/icon_cache) and websockify depend on requests, unpinned. On a
host that already has python3-requests, the installer's venv kept the
distro's idna, requests and urllib3 (Rocky 10: idna 3.7, urllib3
1.26.19; Ubuntu 24.04: idna 3.6, requests 2.31.0, urllib3 2.0.7), all
with advisories, and the panel loads them with its templates. Checked
in fresh Rocky 10 and Ubuntu 24.04 containers with python3-requests
installed first: all three come from the venv.
The installer's venv sees the system site packages, and pip keeps a
system package that meets a requirement. On openSUSE Leap 15.6 the LDAP
login thus ran on the distro's python-ldap 3.4.4 (CVE-2025-61911, a
filter-escaping bypass, and CVE-2025-61912) and pyasn1 0.5.0
(CVE-2026-30922 and others), which django-auth-ldap loads through
ldap.controls. python-ldap>=3.4.5 and pyasn1>=0.6.4 make pip install
fixed copies in the venv. python-ldap builds there, so openldap2-devel
joins the openSUSE package lists (installer, manual and development
setup) and the update steps. Checked in a fresh Leap 15.6 container
with the installer's package list: 3.4.8 and 0.6.4 from the venv,
ldap.controls imports.
1.7.4 no longer preselects a device in the otp_device field. The login
form chooses the user's confirmed TOTP device itself and ignores that
field, so the login is unchanged (OTP login tests pass).
3.18.2 hardens content negotiation against large Accept headers
(GHSA-33wh-fxxf-88vv) and refuses non-text charsets
(GHSA-3547-9m27-7rxg); the API reads both. 3.18 drops Django < 5.2 and
reports list serializer errors as a dict; the API's many=True
serializers only render output.
Security release of 2026-10-06. Two of its fixes reach this app: a
quadratic HTTP header parsing DoS reachable without a login
(CVE-2026-84429) and long language codes through LocaleMiddleware
(CVE-2026-77050). The GeoDjango and model formset fixes do not apply.
Files that libguestfs creates have no SELinux label. On an enforcing
guest (RHEL, Rocky, Alma, Fedora) sshd may not read the unlabeled
/root/.ssh/authorized_keys, so a key added from the panel did not let
anyone log in ("avc: denied { read } ... unlabeled_t").
After writing a key, gstfsd now relabels /root/.ssh with the guest's own
file contexts (/etc/selinux/<SELINUXTYPE>/contexts/files/file_contexts),
as virt-customize does. Guests without an SELinux policy or with SELinux
disabled are left as they are. If the guest needs labels but the host's
libguestfs cannot set them, the request fails before anything is written.
A root password needs no labelling: /etc/shadow keeps its file and label.
gstfsd mounted every partition it listed: a root on LVM was never found,
a disk with two OS roots got the change twice and two concatenated
replies, and no root, an error or a request longer than 1024 bytes ended
the connection without a reply. It now lets libguestfs inspect the guest,
mounts its one OS the way the guest does (a broken filesystem off the
changed path is left out), and sends exactly one reply, keeping the first
error. The root entry is found on any line of /etc/shadow and must be
unique; a key needs no shadow file. A request is at most 64 KiB. Each
change runs in a worker process with a directly started appliance; both
are killed when the request is not done 100 s after gstfsd accepted it
(GSTFSD_DEADLINE may only lower that), before the panel's 120 s timeout
releases the VM, or when the panel's connection ends, and the reply waits
until they are gone, so nothing touches the disk once the panel has given
up. A worker that dies says why. The panel
connects over IPv4 or IPv6, sends the whole request and reads the reply
until gstfsd closes.
Nothing limited failed logins: a password could be guessed at the speed
the server answers. django-axes (8.3.1, Django 5.2, Python 3.10-3.13)
now counts them.
- A user is locked for the address the failed logins come from
(username and IP together), so an attacker cannot lock someone out
from elsewhere and users behind one NAT address do not share a lock.
A successful login resets the count. axes sits in front of every
authentication: the login page with or without OTP, LDAP and the API.
- The number of failed logins (default 5) and the lock time in minutes
(default 15) are on the Settings page (LOGIN_FAILURE_LIMIT,
LOGIN_LOCKOUT_MINUTES, added by migration appsettings.0014) and apply
at once; an invalid value falls back to the default. Unlock with
"manage.py axes_reset_username <username>".
- Behind the bundled nginx every request comes from 127.0.0.1; the
client address is read from X-Real-IP, which nginx sets, but only for
requests from a loopback address or from LOGIN_TRUSTED_PROXIES, so a
forged header does not escape a lock.
settings.py.template gets the app, middleware, backend and AXES_*
settings; README tells existing installations what to copy. Tests that
call client.login now pass a request, which the axes backend needs.
The new tests fail with axes disabled.
Comments, docstrings and live test names cited item IDs of a roadmap
that is not part of the repository. They now say what is meant, or
nothing where the sentence stands without them. Test assertions and
behavior are unchanged.
ldap3, markdown, zipp, qrcode and tzdata are not imported anywhere and
no installed package needs them on Linux with Python 3.10-3.13: LDAP
login uses python-ldap through django-auth-ldap, QR codes come from
django-qr-code (segno), qrcode is only an optional django-otp extra,
tzdata is a Windows-only Django dependency and TIME_ZONE is UTC.
python-engineio stays pinned next to python-socketio, which requires it.
The test suite passes with the five packages uninstalled.
django-login-required-middleware is dropped; Django 5.1+ ships the same
middleware. The login page is already marked login_not_required by
Django; email_otp gets the decorator, which replaces
LOGIN_REQUIRED_IGNORE_VIEW_NAMES. The datasource still requires login.
Existing settings.py files still name the old middleware. A system
check (accounts.E002) stops migrate and the server until MIDDLEWARE
uses Django's middleware, instead of an ImportError at startup; the
README has the steps.
A new matrix test walks every URL anonymously: everything outside a
short allowlist (login pages, the OpenAPI docs) sends the visitor to
the login page, and API views deny with 401/403. It passed with the old
middleware first and passes unchanged now.
Django 4.2 reached end of life on 2026-04-07.
- Django==5.2.17; django-login-required-middleware 0.9.0 keeps working
(the authorization matrix passes).
- USE_L10N removed from the settings template.
- Log out is a POST form in the navbar; Django 5 removed GET logout.
- pip-audit in CI no longer ignores the Django 4.2.30 advisories.
- README: Django 5.2, backup step and upgrade notes for existing
settings and GET logout callers.
python -Wd manage.py test shows no Django deprecation warnings. The live
libvirt suite passes on 5.2 and on 4.2 against a RHEL qemu-kvm host.
- Runs only when TEST_LIBVIRT_HOST is set, so manage.py test on a
developer machine no longer creates VMs on the local libvirtd.
- Every VM and volume is named wvc-test-*, and volumes live in a
temporary wvc-test pool (/var/lib/libvirt/wvc-test) instead of the
default pool next to real images.
- Cleanup removes only prefixed domains and the test pool's volumes;
it also clears leftovers of an interrupted run before starting, and
tolerates domains that turned transient.
- tearDownClass fails if any domain or volume outside the test
namespace appeared, disappeared or was replaced.
- livetest gains create_volume/define_vm helpers and
test_live_dataloss.py, whose setUpClass opens the class transaction
last, so a failed libvirt connection cannot lock the test database
for later test classes.
The first live tests reproduce two known external-snapshot data losses
and are marked expectedFailure: reverting deletes a disk added after
the snapshot, and deleting the older of two snapshots deletes the
newer one's overlay.
- Pick the test VM by name; .first() could select any VM already on
the compute, and tearDownClass deletes the chosen VM's disks.
- Delete the test volume when setUpClass fails after creating it.
- test-vm.xml: no hard-coded emulator, machine alias q35 instead of
pc-q35-5.1, so it defines on RHEL qemu-kvm hosts too.
- Follow current behavior: power actions are POST-only, the create
form needs add_cdrom/add_input and VNC, cloning needs change
permission on the source VM.
- README: the tests read TEST_LIBVIRT_HOST/TYPE/LOGIN/PASSWORD;
WEBVIRTCLOUD_TEST_LIBVIRT_URI never existed.
- Matrix 3.10-3.13; pip cache keyed on the requirements.
- New steps: makemigrations --check, spectacular --validate
--fail-on-warn, check --deploy --fail-level ERROR, pip-audit of the
installed environment (the Django 4.2.30 advisories are ignored until
the Django 5.2 upgrade), and a Docker image build.
- The separate vrtManager unittest run is dropped; manage.py test
already runs those tests.
- CodeQL also runs on pull requests to master.
- Docker installs into a plain venv instead of using the distro's
python3-lxml and python3-libvirt; lxml>=6.1, since the distro's
lxml 5.2.1 has CVE-2026-41066.
- dev requirements add ruff and pip-audit.
- Add the public key to authorized_keys instead of replacing the file.
- Use AF_INET6 when GSTFSD_BIND_HOST is an IPv6 address (binding ::1
crashed with the AF_INET server).
- Close idle client connections after 30 s.
Tests run the real daemon against a small fake guestfs module over a
socket; this also exercises the earlier sendall() fix.
gstfsd only listens on loopback unless GSTFSD_BIND_HOST and
GSTFSD_ALLOW_REMOTE=1 are set; add commented environment lines for both
service definitions. gstfsd.conf now ends with a newline, which matters
because dev/libvirt-bootstrap.sh appends it to /etc/supervisor.conf.
- Hash the root password with a random SHA-512 salt instead of the fixed
"$6$kgPoiREy".
- Talk to gstfsd through one helper with a 120 s socket timeout; an
unreachable daemon or a bad reply is an error message, not a 500.
- gstfsd refuses to listen on a non-loopback address unless
GSTFSD_ALLOW_REMOTE=1: it is an unauthenticated root service.
- gstfsd: encode replies (sendall(str) raised TypeError on Python 3, so
the success path never worked) and use octal chmod modes.
- README: what gstfsd does and how to allow it on a remote compute.
- Comment out the nginx /socket.io/ location: a firewall on 6081 does not
protect a console that nginx proxies on 80/443.
- Default SOCKETIO_HOST to 127.0.0.1.
- README: short enable steps and the changes existing installs need.
- test_socketiod_disabled writes its settings module to a temporary
directory instead of the source tree.
socketiod does not validate the console token, keeps a single global PTY
and broadcasts its output to every connected client, so anyone who can
reach port 6081 can read and write a serial console that someone opened.
It is also broken for legitimate users, since console/views.py now writes
signed tokens that socketiod cannot parse.
Until it is rewritten, turn the serial console off by default:
- Add SERIAL_CONSOLE_ENABLED = False to the settings template; installs
whose settings.py lacks it are treated as disabled.
- The console view shows an error instead of the xterm page for pty
consoles while the setting is off.
- socketiod refuses to start unless SERIAL_CONSOLE_ENABLED is True.
- Supervisor: autostart=false for socketiod; drop -d (debug) from the
supervisor and systemd definitions; fix the systemd unit description;
conf/systemd/install.sh no longer suggests enabling socketiod.
- README: stop opening port 6081 and document how to enable the serial
console on a trusted network.
- Computes API: mark password as write_only in ComputeSerializer, restrict ComputeViewSet and architectures to IsAdminUser.
- Instances API: enforce user ownership tenant isolation in InstanceViewSet.list, restrict MigrateViewSet and CreateInstanceViewSet to IsAdminUser, require IsAdminUser for mutating FlavorViewSet.
- Storage, Network, and Interface APIs: restrict StorageViewSet, VolumeViewSet, NetworkViewSet, InterfaceViewSet to IsAdminUser.
- AppSettings & NWFilters: protect appsettings and nwfilter views with @superuser_only (403 Forbidden for non-superusers).
- noVNC Console: sign tokens with TimestampSigner (salt='console.novnc', max_age=600), verify signature and user authorization in novncd with backward-compatible fallback for debug/test mode.
- Accounts: avoid hardcoded default admin password; generate cryptographically secure random password unless ADMIN_PASSWORD is set (preserve 'admin' in test runner).
- gstfsd daemon: bind to 127.0.0.1 by default instead of 0.0.0.0.
- Tests: added comprehensive test coverage for API security, token expiration/rejection, and view permissions (132 tests passing).
- Upgraded Django to 4.2.30 (latest Django 4.x LTS release)
- Upgraded production dependencies in conf/requirements.txt:
* django_bootstrap5 to 26.2 (latest supporting Django 4.2)
* django-otp to 1.7.3
* django-auth-ldap to 5.3.0
* djangorestframework to 3.17.2 (latest supporting Django 4.2)
* drf-nested-routers to 0.95.3
* drf-spectacular[sidecar] to 0.30.0
* eventlet to 0.41.2
* gunicorn to 26.2.0
* markdown to 3.10 (supporting Python 3.10+)
* paramiko to 5.0.0
* python-engineio to 4.14.0
* python-socketio to 5.17.0
* whitenoise to 6.12.0
* zipp to 4.1.0
- Upgraded development dependencies in dev/requirements.txt:
* coverage to 7.16.1
* django-debug-toolbar to 6.3.0 (latest supporting Django 4.2)
* pycodestyle to 2.15.0
* pyflakes to 4.0.0
* pylint to 4.0.9
* black to 26.5.1
- Remove obsolete spice-html5 static assets and SPICE console templates
- Restrict default console types to VNC with migration 0012
- Filter out SPICE in VM creation form and backend capabilities
- Return friendly deprecation notice if legacy VM is opened with SPICE
- Clean up docs, architecture references, and libvirt-bootstrap scripts
- Prevent baking SECRET_KEY into Docker image during collectstatic
- Persist runtime SECRET_KEY in data/secret_key volume or read from environment
- Remove sed-based injection of CSRF_TRUSTED_ORIGINS in favor of native os.getenv
- Avoid overwriting system python3 symlink on SUSE; auto-detect python 3.10+ binaries
- Make /etc/hosts entry in installer idempotent
- Add security notice regarding compute node VNC/SPICE port firewall isolation
- Add python3-libvirt to apt and configure --system-site-packages for zero-compilation build
- Introduce 10_webvirtcloud_init.sh to handle container startup, migrations, and SQLite persistence
- Exclude host settings.py in .dockerignore for clean builds from settings.py.template
- Add docker-compose.yml with persistent data and SSH volume mounts
- Document Docker Compose deployment in README.md
- Add local ports and env var fallback for CSRF_TRUSTED_ORIGINS in settings.py.template
- Configure SECURE_PROXY_SSL_HEADER for reverse proxy / HTTPS environments
- Update Nginx proxy headers to use $http_host and $scheme to prevent port mismatch
- Relax libvirt-python and lxml version pins to allow precompiled distro packages
- Add PEP 508 environment marker to crypt-r (python_version >= 3.13) to avoid unnecessary C compilation on Python 3.12
- Add stdlib crypt fallback when crypt_r is not available in instances/views.py
- Make LDAP imports and configuration optional in settings.py.template
- Add --system-site-packages and missing packages in webvirtcloud.sh for RHEL/CentOS
- Update README with working Rocky Linux / RHEL / Fedora installation and local dev setup