A backup server serving two datastores is two devices, which the duplicate guard deliberately allows -- but both wanted a token called joulenap, so setting up the second one deleted and recreated the first one's token. The first device was left holding a dead secret, and re-entering the new one did not repair it: deleting a token also drops its ACL entries, and provisioning re-grants only /datastore/<its own datastore>, so the other datastore stayed locked out until root ran acl update by hand. The product supported a configuration its own wizard could not provision. Tokens on a backup server are now named joulenap-<datastore>, sanitised to the character set PBS accepts and falling back to the bare prefix if nothing survives. The two never meet, and each keeps the narrow per-datastore grant rather than widening to /datastore. A Proxmox host is a single device and cannot collide with itself, so its token stays plain joulenap. The name is derived rather than exposed: a field would only invite tokens Joulenap later fails to find. Tokens already in use are untouched. The conflict dialog no longer claims the name is "joulenap", since on a backup server it is not. A device card also stops reporting "Connected - API OK" for what is a one second TCP connect to the API port. The authenticated call behind it is made and its failure discarded, so a server whose credential had been revoked advertised itself as healthy indefinitely, with cached usage figures beside it to match. The label now reads "Reachable", which is what is actually checked; the Test button, which surfaces the same call's error, owns the API verdict. Changing the underlying field was rejected: it is a published contract, both in the dashboard payload and as joulenap_pbs_online, documented as answering on the API port. Documented in the architecture, the wizard guide and the example config, including that replacing a token clears its permissions -- so a hand-made setup where one token served several datastores needs re-granting.
13 KiB
Joulenap — Setup wizard & field discovery
How devices are configured. The goal: ask as little as possible by discovering most values automatically. The wizard may ask for root credentials of a PVE (and optionally of a PBS); with those it creates its own scoped tokens and installs its SSH key, then discards the passwords.
The wizard adds devices, one at a time — it never creates routes. Once a PVE and a PBS exist, you build routes between them from the homepage. Both flows open from Settings → Devices → + Add (or from the first-run banner).
Flow A — Add a PVE
- Connect. Host/IP, port (8006), and either an API token (id + secret) or root credentials. In root mode Joulenap creates a minimal-privilege token for itself and discards the password. TLS verification defaults off, because a stock PVE serves a self-signed certificate. You also pick the device name (its id, used everywhere in the UI); it is pre-filled and you can usually leave it.
- Discovery. Joulenap reads the PVE's storage config and lists every PBS-backed storage on it, deriving each one's host, port, datastore and certificate fingerprint. Each storage falls into one of two groups:
- it matches a PBS you already registered (same host + datastore) → it is linked automatically, filling that PVE's
storagesmap so backup routes to that box become possible. Only storages that exist on the PVE at this moment are linked; register a backup server later and you complete the map with Re-read from Proxmox (below) rather than by re-running this wizard, which refuses a host it already knows; - it is a backup server Joulenap has never seen → you can configure it now, which folds flow B's PBS steps into this run. One per pass; add the rest from Settings → Devices → + Add.
The number of nodes is reported here too — that is how a cluster is detected, and it is why a PVE has no
nodefield to fill in.
- it matches a PBS you already registered (same host + datastore) → it is linked automatically, filling that PVE's
- Configure the new PBS (skipped when there is nothing new): the PBS half of flow B, below.
- Finish. The devices are written and the report lists what was created and which storages were linked.
Flow B — Add a PBS
- Connect. Host/IP, port (8007), datastore, and an API token — or root credentials, from which Joulenap provisions a scoped token. The certificate fingerprint is read from the box and filled in for you on connect; from then on every API call is pinned to it. Also here: the device name, and "Joulenap manages this box's power". Turn that off for an always-on PBS — steps 2 and 3 then do not apply and are skipped.
- Wake-up. The WoL broadcast interface (leave it on
autounless you have several networks) and the MAC address, detected by pinging the box and reading the ARP table — so the PBS must be powered on while you set it up. A Test button sends one real magic packet and names the broadcast address it used: Wake-on-LAN left un-armed in the BIOS is the single most common cause of a failed run, and finding that out at 04:00 is the worst way. - Power-off. Joulenap needs SSH to the box, because PBS has no power-off API.
- The key is get-or-create. All managed PBSs share one
data/id_ed25519, so the wizard returns the existing public key rather than generating a new one — regenerating would silently orphan theauthorized_keysline on the box you added first, and nothing in the UI would tell you. - The host key is confirmed first. The wizard scans the PBS SSH host key, shows its SHA256 fingerprint, and only saves it to
data/known_hostsonce you confirm. Every later connection verifies against it. Automatic key installation refuses to run before this, because that is when a root password would be sent over the connection. - With root credentials the public key is installed for you; otherwise paste the shown line into
/root/.ssh/authorized_keyson the PBS yourself. It is a restricted line with a forced command that only allows the power-off.
- The key is get-or-create. All managed PBSs share one
- Verification. A report of what was configured, plus a warning if this PBS is not the target of any PVE storage — a box nothing can back up to yet.
Field discovery
For each field: auto = discovered/derived, manual = entered.
| Field | Notes | |
|---|---|---|
| PVE host/IP, port | manual | the entry point |
| PVE TLS verify | auto | off when the certificate is self-signed |
| PVE auth | manual | a token, or root once → token created and the password discarded |
| PVE → PBS storage map | auto | from /storage filtered to type=pbs; you pick which to link or configure |
| PBS host/IP, port | auto | read from the PVE storage config (manual in flow B) |
| PBS datastore | auto | from the storage config |
| PBS fingerprint | auto | from the storage config, or read from the PBS certificate on connect |
| WoL broadcast interface | auto | the NIC with the route to the PBS subnet; override allowed |
| PBS MAC | auto | ping + read ARP, with the PBS powered on ("Detect MAC") |
| PBS API token | manual, or auto in root mode | see the privileges below |
| PBS SSH host key | auto | scanned, shown, saved to data/known_hosts on your confirmation |
| SSH user + key | mixed | user defaults to root; the key is Joulenap's own, installed for you in root mode |
| Wake-on-LAN in the PBS BIOS/OS | manual, always | ethtool -s <nic> wol g, made persistent |
If you decline root credentials
Everything above is still discovered with a read-only token; only these become manual:
- On the PVE: create an API token whose role has
VM.Audit, VM.Backup, Datastore.Audit, Datastore.AllocateSpace, Datastore.Allocate(the last is required for vzdump's retention/prune, which deletes old backups); copy the secret. - On the PBS: create an API token with
DatastoreAdminon the datastore (status, GC, verify) andAuditon/system(node CPU/RAM for the dashboard); copy the secret. Name it per datastore — Joulenap's own wizard usesjoulenap-<datastore>— so a second datastore on the same box can have its own. Add the/remoteroles below if this box will take part in a sync route. - On the PBS: install Joulenap's generated SSH public key into
/root/.ssh/authorized_keys. - In Joulenap: paste both tokens, confirm the key is installed, click "Detect MAC".
Tighter PBS privileges (optional): root mode grants the built-in
DatastoreAdmin, because PBS cannot create custom roles over the API. For a truly minimal token, create a role on the PBS host once and bind a token to it, then paste that token:proxmox-backup-manager role create Joulenap --privs "Datastore.Audit,Datastore.Modify" proxmox-backup-manager user generate-token root@pam joulenap-<datastore> proxmox-backup-manager acl update /datastore/<datastore> Joulenap --auth-id 'root@pam!joulenap-<datastore>'
Token names on a backup server carry the datastore —
joulenap-backup,joulenap-offsite. A backup server can hold several datastores, and each is its own device in Joulenap; naming both tokens the same would mean setting up the second one deleted and recreated the first one's token, leaving that device unable to connect. A Proxmox host is a single device, so its token stays plainjoulenap.Replacing a token also clears the permissions granted to it. If you ever confirm a replacement, re-entering the new secret is not always enough: provisioning re-grants only
/datastore/<that device's datastore>, so a hand-made setup where one token served several datastores needs theacl updateabove re-running as root for the others.
Adding a box you already have
An API token belongs to one server: root@pam!joulenap on one machine has nothing to do with a
token of the same name on another. Adding a second Proxmox host or a second backup server
therefore cannot disturb the ones you already have — different box, different token.
The one thing that can go wrong is pointing a wizard at a box that already has a token by that name, because a token's secret exists only at creation: the only way to get a usable one under a name in use is to replace it, and every other holder of the old secret breaks silently.
Joulenap now heads that off from both sides:
- The same box, already registered is refused at the connection step, before any password or token is sent, naming the device you already have. Adding it again would have replaced its token and left the original entry unable to connect. Edit that device instead. (A backup server serving a second datastore is a legitimate second device and is still allowed — it shares a token with the first, so the warning below applies to it.)
- The same box, used by something else: replacing the token asks first, and the confirmation names what Joulenap can see would break — another of its own devices on that host, and any Proxmox host whose PBS storage entry authenticates with it. That last one is the dangerous case: the storage keeps its old secret and every backup through it starts failing with 401, with nothing pointing at the token as the cause.
Joulenap cannot see every holder of a token — a script of your own, another tool, a second Proxmox host it does not manage. If you have any of those, use Existing API token and paste the secret you already have, or give Joulenap a token of its own under a different name.
A backup server added after its Proxmox host
pves[].storages — which PVE storage points at which backup server — is discovered, never
typed. Flow A fills it in from the PVE's storage config while the wizard is open, so a server
you register afterwards (all of flow B, by definition) starts out unlinked: nothing on that
PVE knows how to reach it, and Backup routes onto it cannot be created. Sync, Verify and
External routes are unaffected — they never go through a PVE storage.
Completing it takes two steps, in this order:
- Add the storage on the Proxmox side (Datacenter → Storage → Add → Proxmox Backup Server), pointing at the same host and datastore as the Joulenap device.
- Settings → Devices → edit the PVE → Storage mapping → Re-read from Proxmox.
Joulenap re-reads the host's storage list with the token it already has and rebuilds the map, matching each storage to a registered server by host and datastore. The map is replaced, not merged, so a storage you removed on the Proxmox side disappears here too — and if that would leave an existing route without a mapping the save is refused and nothing changes.
Re-running the Add-PVE wizard is not the way to do this: it only ever creates, so it refuses a host that is already registered. The refusal happens at the connection step, before any credentials leave the browser and before anything is provisioned.
Sync routes need one extra grant
A sync route makes Joulenap create a remote and a sync job on one of the two boxes, which needs privileges at /remote that a plain datastore token does not have. Root-mode provisioning grants them while it still holds the root ticket.
PBS refuses ACL writes from an API token, answering 400 Unprivileged API tokens can't set ACL items. So a box whose token you pasted by hand — or one configured with a Joulenap older than 1.0 — does not have them, and its sync routes will fail until it does. Because only root can make the grant, adding it takes a root login one more time:
Settings → Devices → edit the PBS → Sync routes → Grant sync permissions. Enter the PBS root credentials and confirm. Joulenap logs in as root, adds the two roles to the token the device already uses, and discards the password — the token itself is untouched, so nothing else needs re-entering.
The equivalent on the PBS itself, if you would rather not hand the password over:
proxmox-backup-manager acl update /remote RemoteAdmin --auth-id 'root@pam!joulenap-<datastore>'
proxmox-backup-manager acl update /remote RemoteSyncPushOperator --auth-id 'root@pam!joulenap-<datastore>'
Both roles are needed: RemoteAdmin alone does not cover a push sync.
Security
Transport security uses pinning and verification to protect credentials in transit:
- PBS API (TLS pinning): every PBS's API calls are pinned to that device's stored certificate fingerprint, captured on connect. If a certificate is renewed, re-run the connect step for that device to store the new fingerprint; calls fail with a clear "fingerprint changed" error until you do.
- PBS SSH (host-key verification): each PBS's SSH host key is confirmed once during setup and saved to
data/known_hosts. All later connections verify against it. - PVE setup (residual): a PVE root password used for provisioning is protected by that device's
verify_tlsonly — no fingerprint is stored. Enable it if your PVE has a valid certificate, or keep the setup network trusted (isolated LAN/VPN). - Root credentials are never stored. They are used for the one call that needs them (mint a token, install a key) and discarded with the modal.