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.