Skip to content

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.