# Changelog All notable changes to Depl0y will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.2.99] - 2026-10-11 ๐Ÿ“ HA rules on the HA page ### Added - **HA rules management** (Proxmox VE 9). The HA page shows a **Rules** tab on VE 9 clusters, where HA groups no longer exist: create, edit, enable/disable and delete *node-affinity* rules (nodes with priorities, optionally strict) and *resource-affinity* rules (keep HA resources together or apart). Rules made by the Failover Wizard are marked, and deleting one warns why it exists. On VE 8 the Groups tab is shown as before. - API: `GET/POST /api/v1/pve-node/{host_id}/cluster/ha/rules` and `PUT/DELETE .../rules/{rule}` (writes are admin-only). Input is validated before it reaches Proxmox; `GET` reports whether the cluster supports rules. ### Fixed - The Add HA Resource form's **"Migration Target Node"** was never sent to Proxmox; it was kept only in the browser and shown as if it were a setting. It is removed. The resource table's failover node now comes from real settings only: node-affinity rules on VE 9, the HA group on VE 8. - HA group pickers are hidden on VE 9, where sending a group fails. ### Docs - Feature reference and in-app HA documentation cover rules; two new gallery views (32 in total). The screenshot demo's west cluster now runs VE 9 with HA rules. ## [2.2.98] - 2026-10-11 ๐Ÿงน Legacy HA endpoints removed ### Fixed - **The legacy `/api/v1/ha/groups` and `/ha/resources` endpoints acted on whichever Proxmox host was registered first**, over root SSH (`pvesh`), whatever cluster you meant. Nothing in the app used them any more, so they are removed. Use the per-cluster equivalents, which go through the Proxmox API with the selected host: `/api/v1/pve-node/{host_id}/cluster/ha/resources[/{sid}]`, `/api/v1/pve-node/{host_id}/cluster/ha/groups[/{group}]`, and the Failover Wizard (`/api/v1/ha-wizard`). - **`GET /api/v1/ha/status` now covers every active endpoint** instead of the first one: it returns a `hosts` list (protected guests, HA manager state, nodes online, quorum, or an error per endpoint) plus totals. Settings โ†’ High Availability shows one line per endpoint; standalone hosts show "not a cluster, HA not available". - Removed a "Manage HA Groups โ€” coming soon" dialog in Settings that nothing opened. ### Screenshot tooling - `scripts/screenshots/demo/inner.sh` now stops the mock servers, demo backend and file server when it exits; each run used to leave all four running in its namespace. - `scripts/screenshots/capture.py` only runs when executed, not when imported. ## [2.2.97] - 2026-10-11 ๐Ÿ”ง HA page: no more broken "HA Enabled" toggle ### Fixed - **The HA page's "HA Enabled" toggle did the opposite of what it showed.** It read its position from a status the backend never returns, so it always showed *on*; clicking it called the old `/ha/disable`, which SSHed to the **first registered host** and stopped and disabled the HA services on that one node. Turning it "on" always failed with a 422 (it demanded a `proxmox_password` it never used). - Proxmox has no cluster-wide HA switch: `pve-ha-crm` and `pve-ha-lrm` should run on every node, and a guest is protected by being an HA resource. The toggle is replaced by **"HA services: running on N/M nodes"** for the selected cluster, read through the Proxmox API, and a **Start HA services** button that appears only when some online node is not running them (it needs quorum). There is no "disable": to stop protecting a guest, remove it from HA on the Resources tab or in the Failover Wizard. - **Settings โ†’ High Availability** asked for the Proxmox root password, said it was "only used once", and sent it to the same broken endpoint, which ignored it. The card now links to the HA page and the Failover Wizard instead. ### Removed - `POST /api/v1/ha/enable` and `POST /api/v1/ha/disable`. New: `GET /api/v1/pve-node/{host_id}/cluster/ha/services` and `POST /api/v1/pve-node/{host_id}/cluster/ha/services/start` (admin). ## [2.2.96] - 2026-10-11 ๐Ÿค– Unattended signed releases ### Changed - `scripts/release.sh` no longer insists on a terminal. It only needs one when the signing key is password-protected (minisign then asks for the password); with an unencrypted key it signs without a prompt, so releases can be scripted. It now refuses a signing key that is not mode 0600. - The Depl0y release key no longer has a password; it is protected by its file mode on the release machine. The key itself (`minisign.pub`) is unchanged, so installs keep trusting it. ## [2.2.95] - 2026-10-10 ๐Ÿ›Ÿ Failover wizard ### Added - **Failover wizard** (HA โ†’ Failover Wizard, or **Failover** on a VM or container page; admins only). Makes one guest restart on another node automatically if its node fails, using Proxmox's own HA manager, so failover keeps working when Depl0y is down. There is nothing to tune: - every guest is listed as Protected, Ready, Needs disk move, or with the reason it cannot fail over; - readiness checks cover cluster votes (3 nodes, or 2 and a QDevice), quorum, the HA service on each node, disk storage, attached ISOs, unused disks and passthrough devices; - shared storage (Ceph, NFS, iSCSI) gives failover without data loss; ZFS on two nodes gets a replication job every 2 minutes, and HA is enabled only after the first copy. Node-local LVM and directory storage cannot fail over; if the node has shared or ZFS storage, the wizard offers to move the disks there first; - the setup is fixed: one restart then one relocation, the current node preferred and the node with the most free memory as failover node, failback on, strict placement with replication, local ISOs ejected, cluster HA shutdown policy `migrate` if none is set; - the protection can be removed again from the same wizard. Proxmox VE 9 gets an HA node-affinity rule; Proxmox VE 8 an equivalent HA group. - `backend/tests/test_ha_failover.py`: tests for the wizard's decisions and the exact Proxmox calls it makes. ### Removed - The unused `HAWizard.vue` component. Nothing opened it, and it was built on the legacy `/ha/*` endpoints, which always act on the first registered host. ### Docs - Feature reference, README, in-app documentation and the screenshot gallery (three new views) cover the wizard. The in-app HA page no longer lists PBS as storage for running guests. ## [2.2.94] - 2026-10-10 ๐Ÿงน release.sh fixes ### Fixed - `scripts/release.sh` is executable, so `scripts/release.sh` works without `bash`. - `scripts/release.sh` removes its temporary build directory however it ends (a failed step, a wrong signing password, Ctrl-C), instead of leaving the package in `/tmp`. `--dry-run` still keeps the assets for inspection. - A failed signing step now says that nothing was tagged or published and that it is safe to run again. ## [2.2.93] - 2026-10-10 ๐Ÿ” Signed releases ### Security - **Releases are signed with minisign.** Each release now has `SHA256SUMS.minisig`, a signature of `SHA256SUMS` made with the Depl0y release key (public key in `minisign.pub` and [SECURITY.md](SECURITY.md#verifying-releases)). Its trusted comment names the release (`depl0y vX.Y.Z`). - **The in-app updater refuses releases that are not signed by that key**, or whose signature was made for a different version. The key is built into the root-owned updater, so a compromised GitHub account or release page is not enough to push code to Depl0y servers. The updater installs `minisign` if it is missing. This takes effect from the update after this one: this release is installed by the previous updater. - **`install.sh` checks the signature too** before installing, and installs `minisign`. - `scripts/release.sh` signs `SHA256SUMS` (asking for the key password), checks the signature and that `install.sh` and the updater carry the key in `minisign.pub`, and uploads the signature with the release. ## [2.2.92] - 2026-10-10 ๐Ÿงฐ Installer prompt and docs ### Fixed - **`install.sh`'s "reset the admin password?" prompt read from stdin.** When the script was piped (`curl โ€ฆ | sudo bash`), it read the script's own next line instead of the user's answer, so typing YES was silently ignored. In unattended runs it made the upgrade exit at the prompt. It now reads the answer from the terminal, and skips the reset when there is none. - **`uninstall.sh` pointed at old copies on deploy.agit8or.net** for reinstalling and for its own non-interactive instructions. It now points at the scripts in this repository, downloaded and read before running. ### Docs - README, `INSTALL.md` and `DEPLOYMENT.md` described installs and updates coming from deploy.agit8or.net. They now describe the GitHub release flow from 2.2.90: where packages come from, how the updater installs and rolls back, `scripts/release.sh`, and the backup location `/var/backups/depl0y/`. ## [2.2.91] - 2026-10-10 ๐Ÿ” Keys out of the world-readable service file ### Security - **`SECRET_KEY` and `ENCRYPTION_KEY` are no longer stored in the systemd unit.** `install.sh` wrote both into `/etc/systemd/system/depl0y-backend.service`, which every local user can read (and `systemctl show` prints). With them, a local user could forge admin logins and decrypt every stored Proxmox, PBS, BMC and SSH credential. They now live in `/etc/depl0y/config.env` (`depl0y`, mode 0600), and the unit loads that file. - **Existing installs are moved automatically by the in-app updater.** It takes the `ENCRYPTION_KEY` the running backend uses, so stored credentials keep working, and **replaces `SECRET_KEY`**, since it was exposed: everyone has to sign in again once. If the update rolls back, the previous unit file and `config.env` are restored. The same step fixes the unit's `PATH` on installs that only had the virtualenv on it. - If other users have shell access to a Depl0y server installed before this version, treat the old `ENCRYPTION_KEY` as exposed: rotate the stored Proxmox, PBS and BMC credentials (tokens/passwords) on the systems themselves. ### Fixed - **Re-running `install.sh` could make stored credentials unreadable.** It only looked for existing keys in the unit file; on installs that keep them in `/etc/depl0y/config.env` it found none and generated a new `ENCRYPTION_KEY`. It now reads keys the way systemd does (`config.env` when the unit loads it, else the unit). ## [2.2.90] - 2026-10-10 ๐Ÿ” New updater: one root entry point, verified releases ### Security - **In-app updates no longer need broad root access.** The old updater ran `scripts/upgrade.sh` from `/tmp` as `depl0y`, which needed root `cp`, `rsync` and `chown` with any arguments, so anything running as `depl0y` could become root. Updates are now done by a root-owned `/usr/local/sbin/depl0y-update`, the only root command in the new sudo rules (`scripts/sudoers.depl0y`). It takes no paths or versions from the backend: it installs the latest GitHub release, checks it against `SHA256SUMS`, refuses downgrades, writes into `/opt/depl0y` only as the user that owns each directory, keeps a backup, and rolls back if the backend does not come up healthy. - **New installs get the current release.** `install.sh` downloaded its package from this project's server, which was still serving v1.3.7 (end of life, with fixed critical RCEs). It now downloads `depl0y.tar.gz` from the latest GitHub release and checks its sha256. `/api/v1/system-updates/download`, used by older copies of `install.sh`, now redirects to the same release asset. - **The log viewer's `journalctl` rule takes fixed arguments.** It used to allow any arguments as root (for example `--vacuum-time`, which deletes journals). ### Changed - `scripts/release.sh` publishes a release: tag, `depl0y.tar.gz` with a pre-built frontend, `SHA256SUMS`, and the CHANGELOG section as notes. - Update progress is logged to `/var/log/depl0y-update/update.log`. - Existing installs: the first update to this version installs the new updater automatically if the server's sudo rules still allow root `rsync`. Otherwise, run `sudo bash scripts/install-updater.sh` once from a release, then update again. ### Fixed - **Fresh installs had no admin login.** `install.sh` created the admin user before the database tables existed; `create_admin.py` failed with "no such table: users" but the installer still printed "admin / admin". It now creates the tables first and fails loudly. - **Fresh installs could not run system tools.** The installer's service unit set `PATH` to the virtualenv only, so `tar`, `sudo` and other tools were not found (the old in-app update failed with "No such file or directory: 'tar'"). The unit now has a normal `PATH`, and the backend calls `/usr/bin/sudo` by full path. - `install.sh` reported "v1.3.7" and stored `app_version` 1.2.5 whatever it installed. It now uses the version of the release it downloaded. - Installs made with an older `install.sh` whose in-app update fails: re-run the current `install.sh`, which installs the new updater. ## [2.2.89] - 2026-10-10 ๐Ÿ”’ Self-update is admin-only ### Security - **`POST /api/v1/system-updates/apply` now requires the admin role.** Any signed-in user, including viewers, could start a self-update, which downloads a release and runs its upgrade script on the Depl0y host. Viewers and operators now get 403 "Admin privileges required". Checking for updates and reading the update log are unchanged. ## [2.2.88] - 2026-10-10 ๐Ÿ”’ No local shell for cloud template SSH ### Security - **Cloud image template creation no longer runs commands through a local shell.** `_create_cloud_template_automated` built four `ssh โ€ฆ '