mirror of
https://github.com/Joulenap/joulenap.git
synced 2026-08-11 13:21:43 +02:00
Four review passes over the branch — backend, frontend, docs/packaging, security — and the findings each one confirmed. Config and secrets: - omitting a device secret in `PUT /api/config` silently cleared it. `deep_merge` merges mappings key-by-key but replaces lists wholesale, and 1.0 moved devices from mappings into lists, so deleting an `api_token_secret:` line in the YAML editor returned 200 and wiped the token. A stored secret is now carried over when the field is absent entirely; an explicit `""` still clears it. - the pre-migration `.bak` was created by `copyfile` at 0644 and chmod'ed after, leaving a window in which every credential in it was world-readable. Opened 0600 up front, like every other secret-writing path. Frontend: - a failed SSH keygen put both wizards into an unbounded POST loop: the effect depended on the whole setup object, which is a fresh literal every render, and a failure clears the request cache so the retry fired from the next render instead of from a click. - an expanded run's timeline froze at the finish, leaving `poweroff` stuck on "Running" forever, because polling stopped the moment the run did — and the power-off steps are recorded after the run reaches success. - the route editor merged the PVE and PBS id namespaces, which are unique only per list. With a PVE and a PBS sharing a name, clicking the PVE chip lit the PBS one, flipped the badge to Sync and saved a route that synced a box to itself. The draft now carries kind-prefixed keys, as the topology already did. - Add-PVE could not recover when the PBS landed but the PVE create then failed: each retry minted another orphan token on the PBS and re-posted a device that already existed. It now resumes instead of repeating. - the route-enable and guest-include switches had no accessible name. - upcoming occurrences drifted by an hour across a DST boundary — a fixed 86_400_000 ms step is a UTC day, not a calendar day. - a route whose last run aged out of the shared 50-run window claimed it had never run. - the wizard's manual SSH-key step showed the unrestricted public key above the restricted `authorized_keys` line, inviting a copy of the wrong one. Docs: the migration-failure fallback was documented backwards in three files (the file on disk is preserved, but nothing in 1.0 reads the 0.9 sections, so the app boots empty with the banner set); TLS pinning and SSH host-key verification were both documented as unconditional when the runtime degrades without a fingerprint and trusts on first use; the sync-route credential propagation was documented nowhere; plus a dead Localization page, a wrong endpoint for run steps, an omitted session-less endpoint, a self-contradiction about which wizard calls write to disk, a password-change claim that contradicted SECURITY.md, and a stale `[Unreleased]` link.
199 lines
9.7 KiB
YAML
199 lines
9.7 KiB
YAML
# =====================================================================
|
|
# Joulenap — example configuration
|
|
# Copy to config.yaml and fill in your values. config.yaml is git-ignored.
|
|
# All secrets (tokens, passwords, ssh key) live here — keep it private.
|
|
# =====================================================================
|
|
|
|
app:
|
|
language: en # UI language: en, it, ...
|
|
theme: dark # dark | light — also switchable from the header toggle
|
|
port: 8080 # web UI port
|
|
scheduler_enabled: true # global kill-switch: false arms no route at all (manual runs
|
|
# still work). Pause one route with routes[].enabled: false.
|
|
timezone: "" # IANA tz for the scheduler, e.g. "Europe/Rome".
|
|
# Empty -> the TZ env var, then UTC. Set this (or TZ) or
|
|
# "backup at 02:00" runs at 02:00 UTC in a container.
|
|
secret_key: "CHANGE_ME" # random string for session signing
|
|
# Read-only key for the machine-readable endpoints: GET /api/dashboard (dashboard
|
|
# widgets) and GET /metrics (Prometheus). Leave empty to disable both.
|
|
# Generate/rotate it from Settings -> Integrations in the UI.
|
|
api_key: ""
|
|
# Ask GitHub once a day whether a newer Joulenap release exists, and show a footer
|
|
# badge if so. Off by default: the app makes no outbound internet call unless you
|
|
# turn this on (Settings -> Advanced).
|
|
update_check: false
|
|
session:
|
|
https_only: false
|
|
max_age_days: 14
|
|
auth:
|
|
username: admin
|
|
# password is stored hashed (bcrypt); set it on first run via the UI, or
|
|
# pre-hash it here. Never store plaintext. Generate a hash with:
|
|
# docker exec -it joulenap python -m app.hashpw
|
|
# (or, from a local checkout: cd backend && python -m app.hashpw)
|
|
# Quote the value — the hash contains '$' characters.
|
|
password_hash: ""
|
|
|
|
# =====================================================================
|
|
# Devices and routes
|
|
#
|
|
# Devices are listed once and referenced by id. A *route* is "sources -> target +
|
|
# schedule" and covers every reason Joulenap wakes a PBS:
|
|
#
|
|
# backup one or more PVE sources -> a PBS target (vzdump)
|
|
# sync one PBS source -> another PBS target (remote + sync job)
|
|
# external no source -> a PBS target (PVE/PBS run their own
|
|
# scheduled jobs; Joulenap
|
|
# wakes, watches, powers off)
|
|
# verify no source -> a PBS target (verify its snapshots)
|
|
#
|
|
# All three lists start empty: the wizards under Settings -> Devices -> + Add fill in the
|
|
# devices, then you draw routes between them from the homepage. The commented blocks below
|
|
# show the full shape of each entry.
|
|
# =====================================================================
|
|
|
|
# --- Proxmox VE endpoints (where the guests live) ---
|
|
pves: []
|
|
# pves:
|
|
# - id: pve-01 # slug, unique; also the name shown in the UI
|
|
# host: 192.168.1.10 # or pve.lan. A cluster is ONE entry — it proxies its nodes,
|
|
# # which Joulenap discovers at runtime.
|
|
# port: 8006
|
|
# verify_tls: false # self-signed cert on most homelabs
|
|
# # Token privileges: VM.Audit, VM.Backup, Datastore.Audit, Datastore.AllocateSpace and
|
|
# # Datastore.Allocate on the PBS storage. The last one is required for vzdump's
|
|
# # retention/prune (it deletes old backups) — without it, backups run but pruning fails.
|
|
# api_token_id: "root@pam!joulenap"
|
|
# api_token_secret: ""
|
|
# # For each PBS this node backs up to: the storage as named here in PVE
|
|
# # (Datacenter > Storage). Different PVEs may name the same PBS differently.
|
|
# storages:
|
|
# pbs-01: pbs
|
|
|
|
# --- Proxmox Backup Servers (the boxes that sleep) ---
|
|
pbss: []
|
|
# pbss:
|
|
# - id: pbs-01
|
|
# host: 192.168.1.20 # or pbs.lan
|
|
# port: 8007
|
|
# datastore: backup
|
|
# fingerprint: "" # PBS dashboard > Show Fingerprint, e.g. "aa:bb:cc:..."
|
|
# # Leave this empty and the connection to this PBS is NOT pinned and NOT validated —
|
|
# # its API token then travels over TLS nothing has checked. The wizard fills it in.
|
|
# # Token privileges: DatastoreAdmin on the datastore (status, GC, verify) plus Audit on
|
|
# # /system (read-only node CPU/RAM/network for the dashboard). A PBS taking part in a
|
|
# # sync route also needs RemoteAdmin AND RemoteSyncPushOperator on /remote — PBS refuses
|
|
# # ACL writes from a token, so those must be granted on the box itself or by the wizard's
|
|
# # root mode (see docs/CONFIG-WIZARD.md).
|
|
# api_token_id: "root@pam!joulenap"
|
|
# api_token_secret: ""
|
|
# # Set to false for an always-on PBS (a VM, or a cloud-hosted one): Joulenap then only
|
|
# # schedules routes onto it and never touches its power, so everything below is unused.
|
|
# managed_power: true
|
|
# # Wake-on-LAN
|
|
# mac: "00:11:22:33:44:55" # PBS NIC MAC
|
|
# wol_broadcast_iface: "" # interface on the Joulenap host used to send the magic packet, e.g. eth0 (blank = auto-detect)
|
|
# wait_timeout: 180 # seconds to wait for PBS to become reachable (per wake attempt)
|
|
# wol_retries: 2 # EXTRA re-sends after the first: total attempts = wol_retries + 1,
|
|
# # each waiting up to wait_timeout
|
|
# # Power-off (no PBS API for this -> SSH)
|
|
# poweroff_task_wait: 600 # wait up to N s for a running PBS task to finish before power-off (0 = no wait)
|
|
# ssh_user: root
|
|
# ssh_key_path: /app/data/id_ed25519 # dedicated key; ideally restricted to a poweroff command on PBS
|
|
# # How long External routes onto this PBS wait — i.e. how slow this box is.
|
|
# # Both are worst-case timeouts, not fixed delays.
|
|
# external:
|
|
# first_task_wait: 900 # wait at most N s for the first task to appear after wake-up
|
|
# idle_wait: 300 # power off after N s of continuous task silence
|
|
|
|
# --- Routes ---
|
|
routes: []
|
|
# routes:
|
|
# - id: nightly
|
|
# name: Nightly backup
|
|
# color: "#f5a524" # the route's colour in the topology diagram
|
|
# enabled: true
|
|
# notify: true # send notifications for this route (channels are global, below)
|
|
# kind: backup # backup | sync | external | verify
|
|
# # Guests are per source: vmids collide between PVEs, so each source picks its own.
|
|
# # A newly created VM/CT is backed up automatically in "all" mode but NOT in
|
|
# # "include" mode — that list is explicit, so add new guests to it yourself.
|
|
# sources:
|
|
# - pve: pve-01
|
|
# guests:
|
|
# mode: all # all | include
|
|
# list: [] # used when mode = include, e.g. [104, 106, 202]
|
|
# target: pbs-01
|
|
# schedule:
|
|
# time: "04:00"
|
|
# days: [true, true, true, true, true, true, true] # Mon..Sun; at least one must be
|
|
# # true (pause with enabled: false)
|
|
# # Escape hatch for a schedule time+days cannot express (a day-of-month or month
|
|
# # pattern, a step value, a weekday range). A 5-field crontab string; when set it
|
|
# # wins over time/days, and the UI shows it read-only.
|
|
# # cron: "0 4 1 * *"
|
|
# retention: # applied on the target
|
|
# keep_last: 0
|
|
# keep_daily: 7
|
|
# keep_weekly: 4
|
|
# keep_monthly: 6
|
|
# keep_yearly: 0
|
|
# options:
|
|
# mode: snapshot # snapshot | suspend | stop
|
|
# bwlimit: 0 # KiB/s, 0 = unlimited
|
|
# min_free_percent: 0 # abort if the target datastore has less than this % free (0 = disabled)
|
|
# gc: true # garbage-collect the target once this route is done. Two routes
|
|
# # onto the same PBS with gc on both will each run one.
|
|
# verify_after: false # quick verify of this run's new snapshots, before power-off
|
|
# reverify_days: 30 # verify routes only: skip snapshots verified in the last N
|
|
# # days, so the route stays incremental (0 = verify everything)
|
|
#
|
|
# # A sync route copies snapshots from one PBS to another (pull or push).
|
|
# - id: offsite
|
|
# name: Offsite copy
|
|
# color: "#3b82f6"
|
|
# kind: sync
|
|
# source_pbs: pbs-01
|
|
# target: pbs-02
|
|
# sync_direction: pull # pull | push
|
|
# schedule:
|
|
# time: "05:00"
|
|
# days: [false, false, false, false, false, false, true] # Sundays only
|
|
|
|
# --- Maintenance ---
|
|
# GC and post-backup verify are per-route (routes[].options.gc / .verify_after), and a
|
|
# periodic full verification is a route of kind "verify". Only history retention is global.
|
|
maintenance:
|
|
history:
|
|
retention_days: 14 # auto-delete run history + activity-log entries older than N days (0 = keep forever)
|
|
|
|
# --- Notifications ---
|
|
# Delivered via Apprise (one engine, 100+ services). The friendly fields below
|
|
# are turned into Apprise URLs under the hood; use custom_urls for anything else.
|
|
notifications:
|
|
on_success: true
|
|
on_failure: true
|
|
telegram:
|
|
enabled: false
|
|
bot_token: ""
|
|
chat_id: ""
|
|
ntfy:
|
|
enabled: false
|
|
url: "" # self-hosted, e.g. http://192.168.1.30, or https://ntfy.sh
|
|
topic: "" # e.g. homelab
|
|
email:
|
|
enabled: false # SMTP client
|
|
smtp_host: ""
|
|
smtp_port: 587
|
|
smtp_user: ""
|
|
smtp_password: ""
|
|
from_addr: "joulenap@example.com"
|
|
to_addr: ""
|
|
discord:
|
|
enabled: false
|
|
webhook_url: ""
|
|
# Catch-all: any Apprise URL (https://github.com/caronc/apprise) or a plain webhook.
|
|
# e.g. tgram://token/chatid, ntfy://host/topic, gotify://host/token,
|
|
# pover://user@token, slack://..., matrixs://..., json://your-endpoint
|
|
custom_urls: []
|