From 7b53493800546073bd8195490122b6cf72032336 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 2 Oct 2026 22:47:56 +0300 Subject: [PATCH] fix: stop printing secrets during installation - The generated admin password is written to data/admin_password (mode 0600) instead of being printed, so it no longer ends up in the install log or `docker logs`. If the file cannot be written it is printed as before, so the admin is never locked out. - webvirtcloud.sh no longer echoes the generated SECRET_KEY. - install.sh creates the install log with mode 0600. --- README.md | 15 +++++-------- accounts/apps.py | 27 +++++++++++++++++++--- accounts/test_admin_password.py | 40 +++++++++++++++++++++++++++++++++ install.sh | 2 ++ webvirtcloud.sh | 2 +- 5 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 accounts/test_admin_password.py diff --git a/README.md b/README.md index f205b79a..85a709e8 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ source venv/bin/activate pip install -r conf/requirements.txt # 6. Database migrations and static files -# (the first migrate creates the "admin" user and prints its generated password) +# (the first migrate creates the "admin" user; its password is in data/admin_password) python3 manage.py migrate python3 manage.py collectstatic --noinput @@ -161,7 +161,7 @@ source venv/bin/activate pip install -r conf/requirements.txt # 5. Database migrations and static files -# (the first migrate creates the "admin" user and prints its generated password) +# (the first migrate creates the "admin" user; its password is in data/admin_password) python3 manage.py migrate python3 manage.py collectstatic --noinput @@ -209,7 +209,7 @@ source venv/bin/activate pip install -r conf/requirements.txt # 5. Database migrations and static files -# (the first migrate creates the "admin" user and prints its generated password) +# (the first migrate creates the "admin" user; its password is in data/admin_password) python3 manage.py migrate python3 manage.py collectstatic --noinput @@ -269,7 +269,7 @@ mkdir -p data && python conf/runit/secret_generator.py > data/secret_key && chmo ### 4. Migrate and run the dev server ```bash -# The first migrate creates the "admin" user and prints its generated password. +# The first migrate creates the "admin" user; its password is in data/admin_password. # Set ADMIN_PASSWORD beforehand to choose it yourself. python manage.py migrate @@ -347,12 +347,7 @@ sudo zypper install -y dmidecode && sudo systemctl restart libvirtd ### Default Credentials -The first `python3 manage.py migrate` on an empty database creates a superuser named `admin` with a random password and prints it to the console: - -```text -* Generated random admin password: -* Creating default admin user 'admin' -``` +The first `python3 manage.py migrate` on an empty database creates a superuser named `admin` with a random password, written to `data/admin_password` (mode 0600). In Docker: `docker compose exec webvirtcloud cat data/admin_password`. Set `ADMIN_USERNAME` and/or `ADMIN_PASSWORD` in the environment before that first migrate to choose them yourself. Then sign in at `http://`. diff --git a/accounts/apps.py b/accounts/apps.py index 9d13803b..1993f7c6 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -26,6 +26,29 @@ def apply_change_password(sender, **kwargs): print("\033[1m! Don`t forget to remove the option from settings.py\033[0m") +def _store_generated_password(password): + """ + Write the generated admin password to data/admin_password (mode 0600) + instead of printing it, so it does not end up in install logs or + `docker logs` (ROADMAP O-02). Falls back to printing if the file cannot + be written, so the admin is never locked out. + """ + import os + + from django.conf import settings + + path = os.path.join(str(settings.BASE_DIR), "data", "admin_password") + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w") as f: + f.write(password + "\n") + os.chmod(path, 0o600) + print(f"\033[1m* \033[93mGenerated admin password written to {path}\033[0m") + except OSError: + print(f"\033[1m* \033[93mGenerated random admin password: {password}\033[0m") + + def create_admin(sender, **kwargs): """ Create initial admin user @@ -54,9 +77,7 @@ def create_admin(sender, **kwargs): admin_pass = "admin" else: admin_pass = secrets.token_urlsafe(16) - print( - f"\033[1m* \033[93mGenerated random admin password: {admin_pass}\033[0m" - ) + _store_generated_password(admin_pass) print(f"\033[1m* \033[92mCreating default admin user '{admin_user}'\033[0m") admin = User.objects.create_superuser(admin_user, None, admin_pass) diff --git a/accounts/test_admin_password.py b/accounts/test_admin_password.py new file mode 100644 index 00000000..95789060 --- /dev/null +++ b/accounts/test_admin_password.py @@ -0,0 +1,40 @@ +"""ROADMAP O-02: the generated admin password must not be printed to the log.""" +import io +import os +import stat +import tempfile +from contextlib import redirect_stdout +from pathlib import Path +from unittest.mock import MagicMock, patch + +from django.contrib.auth import authenticate, get_user_model +from django.test import TestCase, override_settings + +from accounts.apps import create_admin + + +class GeneratedAdminPasswordTestCase(TestCase): + def _run_create_admin(self, base_dir): + get_user_model().objects.all().delete() + migration = MagicMock(app_label="accounts") + migration.name = "0001_initial" + out = io.StringIO() + env = {k: v for k, v in os.environ.items() if k not in ("ADMIN_PASSWORD", "ADMIN_USERNAME")} + with override_settings(BASE_DIR=Path(base_dir)), patch("sys.argv", ["manage.py", "migrate"]), patch.dict( + os.environ, env, clear=True + ), redirect_stdout(out): + create_admin(sender=None, plan=[(migration, False)]) + return out.getvalue() + + def test_password_is_written_to_a_private_file_not_printed(self): + with tempfile.TemporaryDirectory() as tmp: + (Path(tmp) / "data").mkdir() + output = self._run_create_admin(tmp) + password_file = Path(tmp) / "data" / "admin_password" + + self.assertTrue(password_file.exists()) + password = password_file.read_text().strip() + self.assertEqual(stat.S_IMODE(password_file.stat().st_mode), 0o600) + self.assertNotIn(password, output) + self.assertIn(str(password_file), output) + self.assertIsNotNone(authenticate(username="admin", password=password)) diff --git a/install.sh b/install.sh index 82d49887..964fc34c 100755 --- a/install.sh +++ b/install.sh @@ -84,7 +84,9 @@ LOG_FILE="/var/log/webvirtcloud-install.log" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true if ! touch "$LOG_FILE" 2>/dev/null; then LOG_FILE="/tmp/webvirtcloud-install.log" + touch "$LOG_FILE" fi +chmod 600 "$LOG_FILE" 2>/dev/null || true echo "* Executing installer ($INSTALLER)..." echo "* Output is logged to $LOG_FILE" diff --git a/webvirtcloud.sh b/webvirtcloud.sh index 60c792ee..d1e02174 100755 --- a/webvirtcloud.sh +++ b/webvirtcloud.sh @@ -296,7 +296,7 @@ install_webvirtcloud () { cp "$APP_PATH/webvirtcloud/settings.py.template" "$APP_PATH/webvirtcloud/settings.py" secret_key=$(generate_secret_key) - echo "* Secret for Django generated: $secret_key" + echo "* Secret for Django generated." tzone_escape="$(echo -n "$tzone"|sed -e 's/[](){}<>=:\!\?\+\|\/\&$*.^[]/\\&/g')" secret_key_escape="$(echo -n "$secret_key"|sed -e 's/[](){}<>=:\!\?\+\|\/\&$*.^[]/\\&/g')" novncd_port_escape="$(echo -n "$novncd_port"|sed -e 's/[](){}<>=:\!\?\+\|\/\&$*.^[]/\\&/g')"