Skip to content

First run and configuration

Both peart and peart-portal read the same configuration file, /etc/peart/config.yaml. Copy it from the shipped example and edit it; the example does not run as-is.

sudo install -d -o peart -g peart-portal -m 0710 /etc/peart
sudo install -o peart -g peart-portal -m 0640 config.example.yaml /etc/peart/config.yaml
sudo install -o root -g root -m 0600 /dev/null /etc/peart/peart.env
sudo install -o root -g root -m 0600 /dev/null /etc/peart/portal.env
sudo install -o root -g root -m 0600 /dev/null /etc/peart/render.env

The example is deliberately refused by the configuration validator: the rendering token is a CHANGE_ME placeholder, and there is no default it could silently fall back to. You have to set a real value.

The three environment files are created empty here and filled in later: Running as a service says what goes in each, and Direct install covers the renderer's token. They have to exist before the units start, because each unit names one with EnvironmentFile= and no leading dash, so a missing file fails the unit rather than being skipped.

What lives in /etc/peart

The directory itself is not root's either, and its three permission digits are three different jobs. The owner is peart because something is created in here: the master key is written on first boot by that process, so it needs w on the directory and not only the right to walk through it. The group is peart-portal because the portal walks through, and nothing more -- it opens config.yaml by its full path and never lists what else is here. Others get nothing at all, so the existence of a master.key in this directory is not discoverable by a local account that has no business knowing.

This is not the traverse-only idiom the 0711 on /var/lib/peart uses further down. There, nobody writes through the root and 0711 says exactly that; here the owner does write, and calling the two the same would be false.

The configuration file is not root's. peart and peart-portal run as different users with different primary groups and both read it, so it belongs to the one that writes and carries the group of the one that reads -- the same idiom, for the same reason, as the peart.db further down this page. Nothing for others, because the file carries secrets.

Path Owner Group Mode Who reads it
config.yaml peart peart-portal 0640 Both surfaces, each opening it itself: peart serve as the peart user through the owner bits, peart-portal serve as the peart-portal user through the group bits.
master.key peart peart 0600 The internal surface only, as the peart user. The portal never opens it, and its unit adds InaccessiblePaths=/etc/peart/master.key on top.
peart.env root root 0600 systemd, as root, reading EnvironmentFile= while preparing the unit; the values reach peart serve as environment, so that process never opens the file.
portal.env root root 0600 systemd, the same way, for peart-portal.service.
render.env root root 0600 systemd, the same way, for peart-render.service; it carries the token the sidecar checks on every request.

That is what lets the three environment files stay root:root 0600 while config.yaml cannot: they are read by PID 1 before it drops to the unit's User=, and the service user never needs to see them. A file no process reads has no business in this directory, which is why the table has a reader on every line.

config.yaml holds no secret, and the reason is the row above it: the portal reads that file. Two of its fields would otherwise be secrets the internet-facing surface can read for no purpose -- render.token, which the renderer checks on every request, and webhook.secret, the HMAC key the outbound notification is signed with. Leave both as the placeholder the example ships and set PEART_RENDER_TOKEN and PEART_WEBHOOK_SECRET in /etc/peart/peart.env instead; Running as a service has what goes in each file. Both override whatever the file says, and an unset variable overrides nothing, so an installation that keeps its values in config.yaml still works -- it just hands them to a process that has no use for them.

The rule is not enforced by the gate, and deliberately: deciding which field is a secret takes judgement, and a check that takes judgement stops protecting. It is written here and in the comments of config.example.yaml, which is where someone adding a field would look.

The master.key row is the mode the process itself produces, not one you set: see the note below the permission block further down.

Paths

Every data path below must already exist and be writable by the peart user. So must the directory master_key_path names, because that is where the key is created; the key file itself is the one thing that does not have to exist, since PEART writes it on first boot if it is not there.

db_path: /var/lib/peart/db/peart.db
master_key_path: /etc/peart/master.key
evidence_root: /var/lib/peart/evidence
rendition_root: /var/lib/peart/renditions
report_root: /var/lib/peart/reports
logo_root: /var/lib/peart/logos
avatar_root: /var/lib/peart/avatars
installation_root: /var/lib/peart/installation

master_key_path must never equal, or sit inside, any of the data roots. Boot refuses a configuration that puts it there, because the key must never end up in the same backup as the data it protects.

Ownership and modes, so the portal reads what it should and nothing else

The systemd units already deny the portal the evidence directory and the master key (InaccessiblePaths=), but that is one layer, and it only applies while the process runs under those units. The filesystem has to say the same thing on its own. On a bare-metal install the three users are peart, peart-portal and peart-render, created along with their groups and with peart-work in Direct install; every command below addresses them and none of them creates one. The portal process reads exactly three things: the database (read-write, because sessions and its own audit rows are written through a second connection), the renditions and the generated reports. It never reads the evidence directory or the key.

sudo install -d -o peart -g peart        -m 0711 /var/lib/peart
sudo install -d -o peart -g peart-portal -m 2770 /var/lib/peart/db
sudo install -d -o peart -g peart-portal -m 2750 /var/lib/peart/renditions
sudo install -d -o peart -g peart-portal -m 2750 /var/lib/peart/reports
sudo install -d -o peart -g peart        -m 0700 /var/lib/peart/evidence
sudo install -d -o peart -g peart        -m 0700 /var/lib/peart/logos
sudo install -d -o peart -g peart        -m 0700 /var/lib/peart/avatars
sudo install -d -o peart -g peart        -m 0700 /var/lib/peart/installation
sudo install -d -o peart -g peart-work   -m 2750 /var/lib/peart/work

The master key is not in that list, and needs nothing from you on a new install. PEART creates it on first boot with exclusive creation at mode 0600 and then states that mode again, so the umask cannot widen it; the process runs as peart, so the owner is right for the same reason. There is one case where you do place the file yourself, and it is restoring a key you already had -- Host migration has that, in the order it has to happen.

Four choices in there are worth understanding rather than copying.

0711 on the data root lets the portal traverse into the subdirectories it is allowed into, without being able to list the root itself. It is the traverse-only idiom, and it is why the portal never needs to be a member of the peart group.

The setgid bit (2xxx) on the three shared directories makes files created there inherit the directory's group, so a rendition written by peart lands in the peart-portal group and stays readable without anyone running chgrp by hand afterwards.

0700 on evidence is the line that matters most. The portal is not in the peart group and the directory gives nothing to others, so the boundary holds even if the process is started by hand, outside systemd, by an operator debugging something at two in the morning.

peart-work on the job directory is the one place two processes meet, and only one of them writes. peart prepares a job directory there with the report's HTML and a copy of each piece of evidence it references; peart-render reads those, renders, and answers with the PDF over the same connection that asked for it. That is why the directory is 2750 and not 2770: the group needs to read and traverse it, never to write in it, and the renderer's own unit declares this path read-only. The setgid bit is what hands the shared group down to each job directory created here, which is what the group reads as.

The shipped units declare that membership themselves, SupplementaryGroups=peart-work in both peart.service and peart-render.service, so it is visible where the rest of the process's rights are and does not depend on a usermod nobody audits later. Nothing else is shared: the database, the evidence and the key stay out of the renderer's reach, and its unit still lists only the work directory as writable.

The database file itself

peart migrate creates peart.db, and it creates it with the process umask, which under the shipped units means a file the portal can read and not write. The portal needs to write it: sessions and its own audit rows go through a read-write connection. So after the first migration, set the file explicitly:

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

Then start the portal once and check what SQLite created beside it:

ls -l /var/lib/peart/db/

peart.db-wal and peart.db-shm have to be group-writable too, and they will be: SQLite gives the companions the mode of the database file itself, setting it explicitly so the process umask cannot narrow it. So the chmod 0660 above is the whole step, and a companion with the wrong mode is not a thing to fix in place -- it means the database has the wrong mode, and that is where to correct it. Chmodding a companion instead looks like it worked and then comes undone, because SQLite deletes both companions when the last connection closes cleanly and creates them again, from the database, on the next open.

internal/store/db_companion_mode_test.go measures this rather than assuming it, at two different database modes, because the paragraph you are reading depends on it.

The internal surface is a member of peart-portal for this directory's sake, declared in its own unit as SupplementaryGroups=peart-portal. Both surfaces open peart.db in WAL mode, and SQLite creates the two companions when the first process opens the database and removes them when the last one closes cleanly, so they are created afresh on every boot by whichever process got there first. The setgid bit on this directory puts them in peart-portal either way. Without that membership, a boot in which the portal opens first leaves companions the internal surface is neither owner nor group of, and it cannot open the database at all: unable to open database file (14), restarting on a loop. Nothing orders the two units, so which one opens first is decided at boot. The membership grants nothing new, since peart already owns everything in that group; it only lets the process see what it already owns.

Listen addresses

internal:
  listen_addr: ":8080"
portal:
  listen_addr: ":8081"

Each binary reads only its own section.

TLS

tls: true
accept_insecure_http: false

tls: true means the deployment sits behind a reverse proxy that terminates TLS, and it turns on the Strict-Transport-Security header. Leaving it false refuses to start unless accept_insecure_http is also true, so a plain-HTTP deployment is always an explicit decision. See TLS and reverse proxy.

The rendering sidecar

For a systemd or bare-metal install, point the renderer at the loopback address and the local work directory, and set a real shared token:

render:
  base_url: "http://127.0.0.1:8090"
  token: "…"          # openssl rand -hex 32, and the same value in the sidecar's environment
  work_root: /var/lib/peart/work
  timeout: 60s

The token is the shared secret the sidecar checks on every request; generate it with openssl rand -hex 32 and set the same value here and in the sidecar's PEART_RENDER_TOKEN. The compose defaults (http://peart-render:8090, /work) only resolve inside the container network.

Sessions

session:
  inactivity_timeout: 1h
  absolute_timeout: 12h

Both may be omitted, in which case they fall back to one hour of inactivity and twelve hours absolute. The inactivity timeout cannot exceed a fixed maximum of four hours.

First access

With the configuration in place, start the services (see Running as a service). On first boot PEART generates the master key at master_key_path. The first access to the internal surface requires creating the administrator and enrolling TOTP; there is no default credential.