1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(backup): TLS client certificates persist in Docker and are backed up

libvirt reads a TLS compute's client certificate from the panel user's
~/.pki/libvirt. In Docker that was /var/www/.pki inside the container,
so `docker compose up -d --build` dropped it and every TLS compute
stopped connecting; no backup held it on either install type, although
"Moving to a new machine" says the backup carries what the computes
accept, and the README never said where the certificates go.

Docker keeps /var/www/.pki in a webvirtcloud-pki volume, owned by
www-data and private like ~/.ssh; the backup copies ~/.pki into pki/;
the moving steps put it back; and "Adding a Compute Node" says where the
files go.
This commit is contained in:
catborise 2026-11-06 09:11:43 +03:00
parent 77f85832c9
commit 482f5bc16d
6 changed files with 33 additions and 10 deletions

View file

@ -93,7 +93,7 @@ RUN chmod +x /etc/my_init.d/10_webvirtcloud_init.sh \
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD /usr/local/bin/webvirtcloud-healthcheck
# Declare mountable data directory for persistent SQLite and SSH keys, and the backups of both
VOLUME ["/srv/webvirtcloud/data", "/var/www/.ssh", "/srv/webvirtcloud/backups"]
# Declare mountable data directory for persistent SQLite, SSH keys and TLS certificates, and their backups
VOLUME ["/srv/webvirtcloud/data", "/var/www/.ssh", "/var/www/.pki", "/srv/webvirtcloud/backups"]
WORKDIR /srv/webvirtcloud

View file

@ -270,6 +270,8 @@ A compute node is a KVM host the panel manages over libvirt.
docker compose exec -u www-data webvirtcloud ssh root@<compute-node-ip> true # no password or prompt: ready
```
For TLS connections, put the CA certificate and the panel's client certificate and key in the panel user's `~/.pki/libvirt/` as `cacert.pem`, `clientcert.pem` and `clientkey.pem`, readable only by that user: `$H/.pki/libvirt/` with the installer; in Docker `/var/www/.pki/libvirt/` (the `webvirtcloud-pki` volume), owned by `www-data`.
3. Install or update the `gstfsd` daemon on the compute node (the bootstrap script does this too). It sets the root password and SSH key of a shut-off VM:
```bash
@ -474,6 +476,7 @@ The panel backs itself up every day at 03:15 (cron in Docker, `webvirt-backup.ti
- `db.sqlite3`: a consistent copy taken while the panel runs, checked with `PRAGMA integrity_check`; with PostgreSQL it is left out, so back up that database yourself
- `secret_key`, `node_id` and `settings.py`
- `ssh/`: the SSH keys of the panel's user, which the SSH computes accept
- `pki/`: the TLS client certificates of the panel's user, which the TLS computes accept
- `commit`, outside Docker
The database holds compute passwords and keys: the directories are readable only by the panel's user. They are on the same host as the panel; the [off-site copy](#off-site-copy) keeps them on another server too. A failed backup is reported in `docker logs` or by `systemctl status webvirt-backup` and deletes no older backup.
@ -716,7 +719,7 @@ A version from before v3.0.0 has no worker: `systemctl disable --now webvirt-wor
### Moving to a new machine
A [backup](#backups) moves the panel to a new machine, or back onto lost volumes: the database, the secret key (signed-in users stay signed in), the worker's id and the SSH keys the computes accept. Install the panel there first, the same version or a newer one, then copy the backup directory over and set `b` to it. The backup does not hold `docker-compose.override.yml`, the nginx config or `/etc/default/webvirtcloud`: bring your own lines over yourself. Users sign in with their passwords from the backup; the new installation's `data/admin_password` no longer applies.
A [backup](#backups) moves the panel to a new machine, or back onto lost volumes: the database, the secret key (signed-in users stay signed in), the worker's id and the SSH keys and TLS certificates the computes accept. Install the panel there first, the same version or a newer one, then copy the backup directory over and set `b` to it. The backup does not hold `docker-compose.override.yml`, the nginx config or `/etc/default/webvirtcloud`: bring your own lines over yourself. Users sign in with their passwords from the backup; the new installation's `data/admin_password` no longer applies.
Docker, in the directory you installed from:
@ -728,6 +731,7 @@ docker compose run --rm --no-deps -T --entrypoint /srv/webvirtcloud/venv/bin/pyt
docker compose cp "$b/secret_key" webvirtcloud:/srv/webvirtcloud/data/ &&
docker compose cp "$b/node_id" webvirtcloud:/srv/webvirtcloud/data/ &&
{ [ ! -d "$b/ssh" ] || docker compose cp "$b/ssh/." webvirtcloud:/var/www/.ssh/; } &&
{ [ ! -d "$b/pki" ] || docker compose cp "$b/pki/." webvirtcloud:/var/www/.pki/; } &&
docker compose up -d # the container migrates and fixes ownership and modes at start
```
@ -740,9 +744,10 @@ cd /srv/webvirtcloud &&
venv/bin/python3 -c "import os,sqlite3,sys; [os.remove(p) for p in ('db.sqlite3-journal','db.sqlite3-wal','db.sqlite3-shm') if os.path.exists(p)]; sqlite3.connect(sys.argv[1]).backup(sqlite3.connect('db.sqlite3'))" "$b/db.sqlite3" &&
cp "$b/secret_key" "$b/node_id" data/ &&
mkdir -p ~www-data/.ssh && { [ ! -d "$b/ssh" ] || cp "$b"/ssh/* ~www-data/.ssh/; } &&
mkdir -p ~www-data/.pki && { [ ! -d "$b/pki" ] || cp -r "$b"/pki/. ~www-data/.pki/; } &&
venv/bin/python3 manage.py migrate &&
chown -R www-data:www-data data ~www-data/.ssh &&
chmod 700 data ~www-data/.ssh && chmod 600 db.sqlite3 &&
chown -R www-data:www-data data ~www-data/.ssh ~www-data/.pki &&
chmod 700 data ~www-data/.ssh ~www-data/.pki && chmod 600 db.sqlite3 &&
systemctl start webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker
```
@ -763,6 +768,7 @@ What an update from an older version changes for operators and users:
- LDAP login's packages are in `conf/requirements-ldap.txt` and no longer installed by default, nor their build headers by the installer; the Docker image keeps them. An existing virtualenv keeps them, but only installing that file updates them (see [LDAP](#ldap)).
- The themes ship compiled; picking one on the Settings page only saves its name, and the "Theme SASS Path" setting is gone. Browsers revalidate static files on each load: files cached before the update can stay until a hard reload (Ctrl+F5) of each page; an external caching proxy may need a purge.
- The panel backs itself up every day ([Backups](#backups)); in Docker into a new volume, `webvirtcloud-backups`.
- In Docker, the TLS client certificates of TLS computes are kept in a new volume, `webvirtcloud-pki` (`/var/www/.pki`), and backed up; put them there once ([Adding a Compute Node](#adding-a-compute-node), step 2).
- The SQLite database must be on a local file system (WAL). Outside compose, stop the Docker container with `docker stop -t 95` so the worker can finish running jobs.
- Long operations run as jobs in a worker process (`manage.py run_worker`): creating, cloning and migrating VMs, cloning volumes, taking and reverting snapshots and deleting external snapshots. The page returns at once and the job panel and the Jobs page follow them; a migration can be cancelled. The API's VM create and migrate answer 202 with the job and need a new `submission_key` per request. The create form no longer takes a console password; set it on the VM page.
- An ISO upload is tracked as a job that can be cancelled. The browser sends the file, so stay on the page until it ends; an upload left idle for an hour is ended and its partial file deleted.

View file

@ -15,8 +15,8 @@ PREFIX = "webvirtcloud-backup-"
class Command(BaseCommand):
help = ("Copy the database, secret key, worker id, settings and the SSH keys of the panel's user into a new "
"directory in backups/, then delete all but the newest --keep backups.")
help = ("Copy the database, secret key, worker id, settings and the SSH keys and TLS certificates of the "
"panel's user into a new directory in backups/, then delete all but the newest --keep backups.")
def add_arguments(self, parser):
parser.add_argument("--keep", type=int, default=7, help="how many backups to keep (default 7)")
@ -73,6 +73,10 @@ class Command(BaseCommand):
for source in ssh.iterdir():
if source.is_file() and not source.is_symlink():
shutil.copyfile(source, to / "ssh" / source.name)
# the TLS client certificates libvirt reads (~/.pki/libvirt); a link stays a link
pki = Path.home() / ".pki"
if pki.is_dir():
shutil.copytree(pki, to / "pki", symlinks=True)
if (settings.BASE_DIR / ".git").exists():
commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=settings.BASE_DIR, capture_output=True, text=True)
if commit.returncode == 0:

View file

@ -63,6 +63,15 @@ class BackupTests(SimpleTestCase):
self.assertEqual(oct((backup / "db.sqlite3").stat().st_mode & 0o777), "0o600")
self.assertEqual(oct((backup / "ssh/id_ed25519").stat().st_mode & 0o777), "0o600")
def test_a_backup_holds_the_tls_certificates_of_the_panel_user(self):
key = self.base / "home/.pki/libvirt/clientkey.pem"
key.parent.mkdir(parents=True)
key.write_text("tls key")
key.chmod(0o600)
copy = self.backup() / "pki/libvirt/clientkey.pem"
self.assertEqual(copy.read_text(), "tls key")
self.assertEqual(oct(copy.stat().st_mode & 0o777), "0o600")
def test_only_the_newest_backups_and_no_partial_one_are_kept(self):
for name in ("webvirtcloud-backup-20260101-000000", "webvirtcloud-backup-20260102-000000",
".webvirtcloud-backup-20260103-000000"):

View file

@ -3,7 +3,7 @@ set -e
APP_DIR="/srv/webvirtcloud"
DATA_DIR="$APP_DIR/data"
mkdir -p "$DATA_DIR" "/var/www/.ssh" "$APP_DIR/backups"
mkdir -p "$DATA_DIR" "/var/www/.ssh" "/var/www/.pki" "$APP_DIR/backups"
# If settings.py doesn't exist, generate from template
if [ ! -f "$APP_DIR/webvirtcloud/settings.py" ]; then
@ -32,9 +32,9 @@ echo "* Running database migrations..."
"$APP_DIR/venv/bin/python3" "$APP_DIR/manage.py" migrate --noinput
# Set proper permissions on runtime and data directories
chown -R www-data:www-data "$DATA_DIR" "/var/www/.ssh" "$APP_DIR/backups"
chown -R www-data:www-data "$DATA_DIR" "/var/www/.ssh" "/var/www/.pki" "$APP_DIR/backups"
chown www-data:www-data "$APP_DIR/webvirtcloud/settings.py" 2>/dev/null || true
chmod 700 "/var/www/.ssh" 2>/dev/null || true
chmod 700 "/var/www/.ssh" "/var/www/.pki" 2>/dev/null || true
# The database holds compute passwords and password hashes; keep it, the
# secret key and settings private to the app user (as the installer does)
chmod 700 "$DATA_DIR" "$APP_DIR/backups"

View file

@ -37,6 +37,8 @@ services:
- webvirtcloud-data:/srv/webvirtcloud/data
# Persistent SSH keys for compute node authentication
- webvirtcloud-ssh:/var/www/.ssh
# Persistent TLS client certificates for TLS computes (.pki/libvirt)
- webvirtcloud-pki:/var/www/.pki
# Daily backups of both (manage.py backup)
- webvirtcloud-backups:/srv/webvirtcloud/backups
@ -45,5 +47,7 @@ volumes:
driver: local
webvirtcloud-ssh:
driver: local
webvirtcloud-pki:
driver: local
webvirtcloud-backups:
driver: local