Verifying the install¶
The binaries¶
peart version
peart-portal version
Release signatures¶
Every release signs its checksum manifest and its SBOM keyless with cosign, published to the public Sigstore transparency log. There is no public key to fetch; the identity is the release workflow. Verify the manifest:
cosign verify-blob \
--certificate-identity-regexp 'https://github.com/mentesan/peart/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--signature checksums.txt.sig --certificate checksums.txt.pem \
checksums.txt
Then check the binaries against the verified manifest:
sha256sum --check --ignore-missing checksums.txt
The SBOM is signed the same way; verify sbom.json with its sbom.json.sig and sbom.json.pem.
The container image¶
The image is signed by its content digest, not by a movable tag:
cosign verify \
--certificate-identity-regexp 'https://github.com/mentesan/peart/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/mentesan/peart@<digest>
The software bill of materials¶
The release SBOM is a CycloneDX document listing every Go module and every vendored front-end package. Read it to see exactly what a given release is built from; Development-lifecycle security explains how it is produced.
The render sidecar got the socket (bare metal)¶
The sidecar runs with no network of its own, and is reachable because peart-render.socket opens the
port outside that isolation and hands the listening descriptor over. Three steps say whether the handover
happened, and each proves a different thing.
systemctl status peart-render.socket
The socket is active and listening on 127.0.0.1:8090. That is the half that opens the port. Seeing
peart-render.service inactive here is expected, not a fault: it starts on the first connection.
journalctl -u peart-render
After the first report generation -- or after the health check the internal surface makes when it starts, which is itself the first connection -- the log carries:
peart-render listening on the socket systemd passed (127.0.0.1:8090)
That sentence, and no other, is what proves the descriptor was inherited. If you instead see
peart-render listening on 127.0.0.1:8090, with no mention of the socket, the service opened the port
by itself inside its own network namespace, and nothing outside will ever reach it. The fix is to enable
the socket and stop enabling the service: Running as a service has the commands, and
Upgrading has the disable an existing
install also needs.
If the service does start, the right line appears, and the connection still does not complete, what is
left is the address filter reaching the inherited socket. Do not remove IPAddressDeny=any or
PrivateNetwork=yes: that is the isolation the renderer exists behind. Report the case instead -- it is
a design decision to make, not an install to patch.
A sidecar warning right after boot¶
With socket activation the service starts on the first connection, and Python with WeasyPrint takes a few seconds to come up. The check the internal surface makes at startup is best effort and not fatal -- a sidecar not yet reachable must not refuse every other route the process serves -- and it is made again, lazily, the first time a report is really generated. So a "sidecar not verified" warning in the first moments after boot resolves itself, and needs no action.
The first report generation (bare metal)¶
Generating one report is the only check that exercises the whole rendering path, and it is the one that
catches a work directory whose group or modes are wrong. The job directory is where two users meet:
peart writes the HTML and the evidence copies, peart-render reads them and writes out.pdf back. If
either side cannot, generation fails and the interface says only that it failed.
journalctl -u peart-render -n 20
The sidecar names the failure and where it happened, for example:
render failed for job 01J8Z...: PermissionError: [Errno 13] Permission denied: '/var/lib/peart/work/01J8Z.../index.html'
A PermissionError there means the group contract is not in place. Check the work directory and the two
memberships:
ls -ld /var/lib/peart/work
id peart
id peart-render
The directory must be peart:peart-work with 2750 -- drwxr-s---, the s being the setgid bit that
puts each new file in the shared group -- and both users must list peart-work. The group reads and
traverses and does not write: the renderer only reads what was staged for it and answers with the PDF
over the connection, so group write was removed and the setgid that hands the group down stayed. First run and
configuration
has the commands.
Look in the journal rather than in the work directory: the job directory is destroyed at the end of every attempt, successful or not, so by the time you go looking there is nothing left to inspect. The log line is the evidence that survives.
Both surfaces come up whichever opens the database first (bare metal)¶
Both surfaces open the same peart.db in WAL mode, and SQLite creates peart.db-wal and peart.db-shm
when the first process opens it, removing them when the last one closes cleanly. The companions are
therefore created afresh on every boot, by whichever unit happened to start first, and nothing orders
the two. That makes the order a coin toss, which is why an install can pass every check on the day it is
built and fail on its first reboot.
Reboot the host and confirm both surfaces answer. Then force the order that is not the lucky one:
sudo systemctl stop peart peart-portal
sudo systemctl start peart-portal
sudo systemctl start peart
systemctl is-active peart peart-portal
Both must report active. When the membership is missing, the internal surface does not start, and
journalctl -u peart -n 20 shows it refusing the database it owns:
unable to open database file (14)
with the unit restarting on a loop behind it. The fix is the group membership, not the file modes:
peart.service declares SupplementaryGroups=peart-portal, and First run and
configuration says why. Check it with id peart, which must
list peart-portal alongside peart-work.
What each end-to-end check proves (bare metal)¶
The checks above each test one mechanism. These four exercise the whole thing, and the reason to run all four is that each one fails for a different reason, so passing one says nothing about the others.
Generate a report. It proves three things at once that a hardened unit is exactly where they break: the sidecar was handed its socket and answered without a network of its own, the PDF metadata step ran without a writable home directory, and the job directory carried the shared group so the renderer could read what was staged for it. A failure here is the one with the most possible causes, which is why When a hardening directive is the cause is indexed by the log line.
Open a piece of evidence. On a migrated install this is the check that matters most, because it is the only one that proves the master key now in place is the one the evidence was encrypted to. A key that does not match fails here and nowhere else, and on an encrypted volume it also proves the volume itself decrypted, which no amount of "the service started" tells you.
Sign in to the portal. It proves the database and the -wal and -shm files beside it are writable
by the portal, because a session is a write. That is a different question from whether the internal
surface can open the database, which is its own
check: the two processes reach
that file by different routes, and each route can be broken alone.
On an encrypted volume, reboot. It proves the volume unlocks without anyone at the console and that the services waited for the mount instead of writing to an empty mount point. Host hardening has the negative side of this test, taking the key server away, which is the half that proves the encryption is actually in force.
The install is working when¶
The internal surface answers on its port behind the proxy, first access prompts you to create the administrator and enrol TOTP, and a generated report renders through the sidecar. If report generation fails on a bare-metal install, the first thing to check is whether the sidecar got the socket; after that, what the journal says about the first generation, and that the sidecar is up and that its token matches on both sides.