diff --git a/README.md b/README.md index 73e19d19..22901e55 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ Sign in at `http://`; you must set a new password at the first login, A compute node is a KVM host the panel manages over libvirt. -1. Install KVM and libvirt. The bootstrap script handles Ubuntu, Debian, CentOS, Fedora, Rocky Linux and AlmaLinux 10, openEuler 24.03 LTS, openSUSE and SLES. On RHEL, which needs EPEL from its release RPM, install the `qemu-kvm` and `libvirt` packages yourself and enable `virtqemud.socket` (plus `virtproxyd-tcp.socket` for TCP): +1. Install KVM and libvirt. The bootstrap script handles Ubuntu, Debian, CentOS, Fedora, Rocky Linux and AlmaLinux 10, openEuler 24.03 LTS, openSUSE and SLES. On RHEL, which needs EPEL from its release RPM, install the `qemu-kvm`, `libvirt` and `python3-libguestfs` (for gstfsd, step 3) packages yourself and enable `virtqemud.socket` (plus `virtproxyd-tcp.socket` for TCP): ```bash curl -fsSL https://raw.githubusercontent.com/retspen/webvirtcloud/master/dev/libvirt-bootstrap.sh | sudo sh diff --git a/conf/requirements.txt b/conf/requirements.txt index 77152c96..8fa9638e 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -23,4 +23,5 @@ python-socketio==5.18.1 segno==1.6.6 websockify==0.13.0 whitenoise==6.12.0 -crypt-r>=3.13.0; python_version >= "3.11" +# the root password's SHA-512 crypt hash (the stdlib crypt is gone in Python 3.13) +libpass==1.9.4 diff --git a/dev/requirements.txt b/dev/requirements.txt index 8a3de2f3..6f1c42ae 100644 --- a/dev/requirements.txt +++ b/dev/requirements.txt @@ -6,6 +6,8 @@ libsass==0.23.0 django-debug-toolbar==8.0.0 django-debug-toolbar-template-profiler==2.1.0 pip-audit==2.10.1 +# the tests read cloud-init's YAML +PyYAML==6.0.3 ruff==0.17.0 # tracebacks of failures in manage.py test --parallel tblib==3.2.2 diff --git a/instances/tests/test_root_pass.py b/instances/tests/test_root_pass.py index 2bcb15bc..4f986af5 100644 --- a/instances/tests/test_root_pass.py +++ b/instances/tests/test_root_pass.py @@ -1,3 +1,4 @@ +from passlib.hash import sha512_crypt from unittest.mock import patch from django.contrib.auth import get_user_model @@ -45,9 +46,10 @@ class SetRootPassTestCase(TestCase): self.assertEqual(compute, self.compute) self.assertEqual(data["action"], "password") self.assertEqual(data["vname"], "renamed-on-host") - # SHA-512 crypt with a random salt, not the old fixed one + # SHA-512 crypt with a random salt, not the old fixed one, which the guest's crypt(3) checks self.assertTrue(data["passwd"].startswith("$6$")) self.assertFalse(data["passwd"].startswith("$6$kgPoiREy$")) + self.assertTrue(sha512_crypt.verify("newSecretPassword123", data["passwd"])) def test_set_root_pass_get_not_allowed(self): response = self.client.get(reverse("instances:rootpasswd", args=[self.instance.id])) diff --git a/instances/views.py b/instances/views.py index d905aafe..d68653d9 100755 --- a/instances/views.py +++ b/instances/views.py @@ -7,17 +7,10 @@ import os import re import secrets import socket -import sys import subprocess import time from ipaddress import ip_address -# the stdlib crypt is deprecated from Python 3.11 and gone in 3.13: crypt_r is the same module -if sys.version_info >= (3, 11): - import crypt_r as crypt -else: - import crypt - from accounts.models import UserInstance, UserSSHKey from admin.decorators import superuser_only from appsettings.models import AppSettings @@ -42,6 +35,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy, gettext_noop from django.views.decorators.http import require_POST from django.views.decorators.debug import sensitive_post_parameters +from passlib.hash import sha512_crypt from libvirt import (VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTOFF, @@ -1076,7 +1070,7 @@ def set_root_pass(request, pk): if request.method == "POST": passwd = request.POST.get("passwd", None) if passwd: - passwd_hash = crypt.crypt(passwd, crypt.mksalt(crypt.METHOD_SHA512)) + passwd_hash = sha512_crypt.hash(passwd) # gstfsd opens the domain by name: send the host's current one. data = {"action": "password", "passwd": passwd_hash, "vname": instance.proxy.instance.name()}