Skip to content

Upgrading

Database schema changes are forward-only migrations, and the process refuses to start against an outdated database rather than migrating silently at boot. So an upgrade is always: get the new version, run the migration, restart. Back up first.

Back up first

peart backup -config /etc/peart/config.yaml -out /path/to/pre-upgrade-artifact

The artifact is encrypted and excludes the master key (see Data directory and backups).

Run the migration

peart migrate applies every pending migration, or none: it never leaves the database half-migrated, so it is always safe to run again. It creates the database if it does not exist and does not mutate anything at boot on its own; migrating is an explicit step.

Bare-metal / systemd:

sudo systemctl stop peart peart-portal
sudo -u peart peart migrate --config /etc/peart/config.yaml
sudo systemctl start peart-render peart peart-portal

Container:

docker compose pull
docker compose run --rm peart migrate
docker compose up -d

Why the process refuses an outdated database

The internal surface checks the schema version at startup and refuses to run if the database is behind, instead of migrating on its own. This is deliberate: an automatic migration at boot is the kind of step that silently changes data on a restart nobody meant as an upgrade. Making the migration an explicit, separately-run command keeps the restart predictable and lets the backup happen first.

The render sidecar now starts through a socket

Bare-metal installs only; the container path is unaffected. The sidecar is started by peart-render.socket, which opens 127.0.0.1:8090 on the host and hands the listening socket to a service that still has no network of its own. Enable the socket and stop enabling the service:

sudo cp deploy/systemd/*.service deploy/systemd/*.socket /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl disable peart-render.service
sudo systemctl enable --now peart-render.socket

The disable matters as much as the rest: a service still enabled on its own starts without the socket, binds its private loopback, and is unreachable again.

If you worked around the unreachable sidecar with a drop-in that set PrivateNetwork=no and IPAddressAllow=localhost, remove it in the same pass with sudo systemctl revert peart-render.service. Until you do, the process that parses HTML derived from hostile content can still reach every service on the host's loopback, the internal PEART API included.

A different workaround can go in the same pass: if you gave the internal service a writable home with a drop-in (CacheDirectory plus Environment=HOME) because every PDF generation ended in panic: config problem: mkdir /home/peart: permission denied, remove it with sudo systemctl revert peart.service. The PDF normalisation no longer uses a configuration directory under $HOME at all, so a service account with no home is what it now expects.

The job directory is now a group contract

Bare-metal installs only, and it applies to every install made before this release: report generation fails until the group exists. peart and peart-render share the same job directory, and the group that lets them is peart-work. The sharing is one-way: peart stages the job there and peart-render only reads it, so the group needs to carry reading and nothing more. Create it, hand the work directory over to it, and restart both units:

sudo groupadd --system peart-work
sudo chgrp peart-work /var/lib/peart/work
sudo chmod 2750 /var/lib/peart/work
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart peart.service peart-render.socket

The units are copied because the membership is declared in them, SupplementaryGroups=peart-work, not left to a usermod on the host. peart-render.service is restarted by restarting its socket; it starts on the next connection.

The container path needs nothing here: both services get the shared gid from the image and compose.yaml. Pull the new images and recreate.

The evidence directory is checked at start-up

Both deployments. The evidence directory holds the originals, and 0700 is the mode configuration has always asked for. From this release the process checks it instead of assuming it: if the directory already exists open to the group or to others, peart serve refuses to start and prints the mode it found and the chmod that fixes it.

This bites the container path in particular. A named volume keeps the ownership and permissions it was first populated with, so a deployment created before this release still carries whatever the old image's build umask left there -- 0755 -- and the new image's own 0700 does not reach it. Run the chmod the refusal prints, against the volume, and start again.

The secrets move out of config.yaml

Every install, and it follows from the permission fix above: config.yaml is now group-readable by peart-portal, because both surfaces open it and until now neither could. That is correct, and it means a config.yaml still carrying render.token and webhook.secret hands both to the process exposed to the internet. Move them:

# put the values in /etc/peart/peart.env, root:root 0600
PEART_RENDER_TOKEN=<the value that was in render.token>
PEART_WEBHOOK_SECRET=<the value that was in webhook.secret, if you use the webhook>

Then put the placeholders back in config.yaml, leave webhook.url where it is (a destination is not a secret), and systemctl restart peart.service. The renderer's own /etc/peart/render.env already carries the same token and does not change. Running as a service has the full contents of each file.

Not moving them is a choice, not an oversight, and this is what it costs: the portal keeps read access to the token the renderer trusts and to the key the outbound notification is signed with. Nothing breaks and nothing warns you.

The renderer stops writing, and the work directory tightens

Bare-metal installs. The PDF now comes back over the connection that asked for it instead of through a file in the job directory, so peart-render reads that directory and writes nowhere under the data tree. Its unit says so, and the directory can lose the group write it only ever had so the renderer could create out.pdf in it:

sudo chmod 2750 /var/lib/peart/work
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart peart.service peart-render.socket

If you have a drop-in setting RestrictSUIDSGID=no on any unit, remove it now with sudo systemctl revert peart.service (and the same for the others that carry one) and reload. That drop-in was the workaround for this defect: generation needed a chmod carrying the setgid bit, which the hardened unit denies. Nothing asks the kernel for that bit any more, so the hardening goes back on.

If an earlier upgrade left the directory at 2770, narrow it back now with sudo chmod 2750 /var/lib/peart/work, which is what First run and configuration installs. The group write it grants is one nobody uses any more, and the process it grants it to is the one that opens HTML derived from hostile content: a mode wider than the thing it protects is how the next install finding starts, and two installs of the same release disagreeing about it is how nobody notices.

The internal surface joins the portal's group

Bare-metal installs, every one made before this release. Both surfaces open the same database in WAL mode, and the companions peart.db-wal and peart.db-shm are created by whichever process opens it first, landing in the peart-portal group through the directory's setgid bit. The internal surface was not a member of that group, so a boot in which the portal opened first left it unable to open the database at all, restarting on a loop, with unable to open database file (14) in its journal. Nothing on the filesystem changes here, no owner and no mode, only the unit:

sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart peart.service peart-portal.service

An install that has never rebooted may never have seen this, because the order is decided at boot and the working order is exactly as likely as the failing one. Never having seen it is not evidence of not having it. Verifying the install has the step that forces the failing order on purpose.

The container path needs nothing: both processes there run as the same user, so neither ever met this.

If you ever chmodded the WAL companions

Only if you followed the old wording in First run and configuration, which said to run chmod 0660 on peart.db-wal and peart.db-shm when they came out wrong. That advice treated the symptom. SQLite takes the companions' mode from peart.db, so a companion that came out wrong meant the database was wrong, and the chmod held only until the last connection closed cleanly: SQLite deletes both companions then and creates them again, from the database, on the next open. The install works, nothing changes, and it fails at a login some time later.

Check the database itself and fix it there:

ls -l /var/lib/peart/db/peart.db
sudo chown peart:peart-portal /var/lib/peart/db/peart.db
sudo chmod 0660 /var/lib/peart/db/peart.db

Nothing to restart: the next open picks it up.

Renditions and generated reports written before this release

Every install that has published a finding with an image, or generated a report, before this release.

The portal serves two things from disk: evidence renditions and generated reports. Both were created 0600 inside directories created 0700, by peart, and both are read by peart-portal, which is a different user. On an install with separate users the portal could neither enter the directory nor open the file, so renditions showed as broken images and the report download answered 500 while the rest of the screen worked.

First run and configuration already told you to create renditions/ and reports/ as 2750 peart:peart-portal. That is right, and it was not enough: the setgid bit gives everything created inside those directories the right group, and the mode came from the process creating them. New files and directories are now 0640 and 0750. What already exists on disk keeps the mode it was born with, and no migration reaches file permissions.

Fix the existing tree. It only touches the two directories the portal serves from, it leaves everything else alone, and it is safe to run again:

sudo find /var/lib/peart/renditions /var/lib/peart/reports -type d -exec chmod 2750 {} +
sudo find /var/lib/peart/renditions /var/lib/peart/reports -type f -exec chmod 0640 {} +
sudo chown -R peart:peart-portal /var/lib/peart/renditions /var/lib/peart/reports

Check it as the portal, not as yourself. Your own account very likely reads those files whatever their mode, which is exactly why this went unnoticed until an install reported it:

sudo -u peart-portal test -r "$(sudo find /var/lib/peart/renditions -type f | head -1)" && echo readable

Nothing to restart.

Do not run the same commands on /var/lib/peart/evidence, on the client logos, on the user avatars or on /var/lib/peart/installation. Those are 0700 by decision: the portal is never given their paths and must not be able to read them.

The trail names who acted

Every install. Until this release, five account transitions -- created, deactivated, deleted, second factor reset, second factor downgraded -- recorded themselves as the CLI acting as the system, with no actor at all, even when an administrator had done them through the interface. Two of the five are not reachable from the CLI at all, so for those the label was never right.

Nothing is rewritten. There is no migration and no way to recover who acted on the rows already written, because the missing field is precisely the actor. What you need to know when reading your own trail: an entry of one of those five actions, dated before this release and labelled cli / system, may have been an administrator acting through the interface. After this release the two are distinguishable.

After upgrading

Confirm the services are healthy (for the container path, docker compose ps should show peart-render as Up (healthy) before report generation works), and that a report still renders through the sidecar. That last one is the check that proves both changes above landed; Verifying the install says what the journal shows when it does not.