Configuration

The patchpanel daemon’s bootstrap configuration — what it is, where it lives, how to edit it, and what every key does.

Table of contents

  1. TOC

What configuration patchpanel keeps where

Patchpanel has two persistent data surfaces. Don’t confuse them.

FilePurposeHow to edit
/etc/patchpanel/config.yamlBootstrap: paths, ports, TLS, auth strategy, log levels, GeoIP feature flag. Read once at startup.Settings page in the UI, or hand-edit + restart.
/var/lib/patchpanel/state.jsonHAProxy data model: frontends, backends, routes, ACLs, certs, providers, peers.Every other page in the UI, or PUT /api/state from a script.

This doc covers config.yaml. For state.json see the Architecture page and the API reference.

File location and lookup order

configLoader resolves the config file in this order (server/src/config/configLoader.js:84):

  1. --config <path> CLI flag passed to patchpanel server
  2. CONFIG_PATH environment variable
  3. <install-root>/dev.config.yaml (developer override — only present in source checkouts)
  4. /etc/patchpanel/config.yaml (the Debian package default)

The first readable candidate wins. The systemd unit sets CONFIG_PATH=/etc/patchpanel/config.yaml explicitly, so on a stock install path #2 is what loads.

Inside the Home Assistant addon the convention is /config/config.yaml on the addon’s persistent volume — set via CONFIG_PATH in the addon’s run.sh.

File format

Patchpanel uses metadata-wrapped YAML: every leaf value is an object with type, value, and rendering metadata. This single schema drives both the runtime config and the Settings UI’s auto-rendered form.

server:
  port:
    type: integer
    value: 8099
    description: TCP port for the management UI and API.
    section: Server
    subsection: Bind
    order: 2
    required: true
    validation:
      min: 1
      max: 65535

configLoader walks the tree and produces two views:

  • getConfig() — flat, values-only. Every leaf’s .value hoisted in place. Application code reads this (config.server.port returns 8099, not the metadata wrapper).
  • getRawConfig() — original metadata tree. The Settings UI and GET /api/config consume this view so the form can render field types, options, validation, and conditional visibility.

A leaf is recognised by having both type: <string> and a value key. Anything missing one of those is treated as a non-leaf and walked deeper.

The top-level _sections block declares Settings-UI section icons + order. It’s skipped during flattening.

Editing the config

Open the patchpanel UI, click Settings. Each top-level section becomes a card with subsection blocks inside. Edits are draft-only until you click Save. Most fields require a process restart to take effect — click the Restart now button next to Save. The UI polls /health for up to 60 seconds and auto-reloads when the server comes back.

Under the hood: the UI POSTs a flat {path: value, ...} patch to PUT /api/config. Each value is validated against the leaf’s schema metadata (type, options, validation.min/max) before disk write. The first UI-driven save against a hand-written config.yaml preserves the original verbatim at <configPath>.preserved-<iso> so any operator-added comments or formatting survive.

Hand-editing the YAML

You can edit /etc/patchpanel/config.yaml directly. Three caveats:

  1. The migrator may add new keys on the next start-up. It’s careful with your value edits but will inject any fields the template adds.
  2. The first save through the Settings UI strips your comments. The UI round-trips the YAML through js-yaml’s dump; comments don’t survive. A copy of the pre-save file is preserved at <path>.preserved-<iso> if the file isn’t watermarked yet.
  3. Restart the process to pick up changes — the daemon caches the config at boot.
sudo nano /etc/patchpanel/config.yaml
sudo systemctl restart patchpanel

Restart endpoint

POST /api/config/restart sends SIGTERM to the running process. The systemd unit (Restart=on-failure, RestartSec=10) or HA addon supervisor brings it back. Use this from automation when applying a config patch via the API.

Reference: every section

The remainder of this page is a reference. Every section in production-config.yaml is reproduced below with what each key does and which subsystem reads it.

version

Schema marker — set by the migrator to match package.json.version. Don’t edit this manually; the migrator overwrites it on every upgrade.

mode

FieldTypeDefaultWhat it does
modeselectstandaloneDeployment surface — standalone (Debian baremetal) or homeassistant (HA addon).

mode is informational and drives path defaults. It does NOT gate auth — auth.strategy does that independently. The Settings UI shows or hides the ingressPathHeader / supervisorTokenEnv fields based on mode.

server.*

The HTTP server’s bind, TLS, and graceful-shutdown behaviour.

FieldTypeDefaultWhat it does
server.hosthost0.0.0.0Bind interface. 127.0.0.1 to bind localhost only.
server.portinteger (1–65535)8099TCP port.
server.trustProxyarray of CIDR strings[]Trusted upstream proxies for X-Forwarded-* and HA ingress headers. The auth middleware uses this for the ha-ingress strategy’s source-IP gate. Only /32 and /128 entries are matched exactly; wider CIDR ranges aren’t supported for the ingress gate (Express’s own trust proxy setting handles forwarded-for unwinding for req.ip).
server.shutdownGracePeriodMsinteger (0–60000)10000Time to drain in-flight requests on SIGTERM before forced exit.
server.ingressPathHeaderstringX-Ingress-PathHeader carrying the HA ingress URL prefix. Only used when mode=homeassistant.
server.supervisorTokenEnvstringSUPERVISOR_TOKENEnv var name holding the HA supervisor token. Used for HA API callbacks.

ssl.*

TLS termination for the management UI itself. Independent of HAProxy’s own TLS — HAProxy reads its own crt-list, set up via the Certificates page.

FieldTypeDefaultWhat it does
ssl.enabledbooltrueServe the management UI over HTTPS.
ssl.generatebooltrueGenerate a self-signed cert on first run if certPath is missing.
ssl.certPathstring/etc/patchpanel/ssl/cert.pemTLS certificate (PEM).
ssl.keyPathstring/etc/patchpanel/ssl/key.pemTLS private key (PEM, mode 0600).
ssl.minVersionselectTLSv1.2Minimum protocol version.
ssl.maxVersionselectTLSv1.3Maximum protocol version.
ssl.cipherstextareaECDHE listOpenSSL cipher list.
ssl.honorCipherOrderbooltrueServer picks cipher from its own preference order, not the client’s.

To run the management UI behind a Let’s Encrypt cert that patchpanel itself issues for HAProxy, point certPath / keyPath at the /etc/letsencrypt/live/<host>/ symlinks and set ssl.generate: false so the daemon doesn’t overwrite them.

paths.*

Every filesystem path the daemon reads or writes. Group by subsection.

Data (/var/lib/patchpanel/)

FieldDefaultNotes
paths.state/var/lib/patchpanel/state.jsonCanonical HAProxy state document — what the renderer consumes.
paths.audit/var/lib/patchpanel/audit.sqliteSQLite audit log of every state mutation.
paths.snapshotsDir/var/lib/patchpanel/snapshotsTime-machine snapshots of the state document.
paths.geoipDir/var/lib/patchpanel/geoipMaxMind / DB-IP MMDB store.
paths.credentials/var/lib/patchpanel/credentialsACME account keys + DNS provider credential files.
paths.optionsnullHA addon options.json path. Null in standalone.
paths.users/var/lib/patchpanel/users.jsonLocal user accounts (bcrypt-hashed passwords). Mode 0600.
paths.apiTokens/var/lib/patchpanel/api-tokens.jsonAPI tokens (bcrypt-hashed secrets). Mode 0600.
paths.setupToken/etc/patchpanel/setup.tokenOne-time first-run wizard token. Postinst generates; wizard consumes + deletes.

HAProxy (/etc/haproxy/, /run/haproxy/)

FieldDefaultNotes
paths.haproxyConfig/etc/haproxy/haproxy.cfgWhere the rendered cfg is atomically swapped.
paths.haproxyCertsList/etc/haproxy/certs.listcrt-list file referenced from bind ssl.
paths.haproxyCertsDir/etc/haproxy/certsCert directory referenced from crt-list.
paths.haproxyMasterSocket/run/haproxy/master.sockMaster CLI socket — used to reload zero-downtime.
paths.haproxyStatsSocket/run/haproxy/admin.sockRuntime stats / admin socket — per-server state, weights, etc.
paths.haproxyPidFile/run/haproxy.pidUsed by the systemctl control strategy.
paths.haproxyBin/usr/sbin/haproxyBinary used for haproxy -c validation.
paths.haproxyErrorPagesDir/var/lib/patchpanel/errorsCustom HTTP error pages.
paths.haproxyMapsDir/etc/haproxy/mapsHAProxy map files (one per state.maps[]).

Keepalived (/etc/keepalived/, /run/)

FieldDefaultNotes
paths.keepalivedConfig/etc/keepalived/keepalived.confRendered keepalived config.
paths.keepalivedPidFile/run/keepalived.pidPID file.
paths.keepalivedBin/usr/sbin/keepalivedBinary.

Cluster (/etc/patchpanel/)

FieldDefaultNotes
paths.nodeConfig/etc/patchpanel/node.yamlPer-node identity (nodeId, VRRP priority overrides). NEVER syncs between cluster peers.
paths.peersStore/etc/patchpanel/peers.jsonPaired peer URLs + tokens. Mode 0600.

Certificates

FieldDefaultNotes
paths.trustedCasDir/var/lib/patchpanel/trusted-casUploaded CA bundles for mTLS validation + upstream verify.
paths.trustedCrlsDir/var/lib/patchpanel/trusted-crlsUploaded CRLs.
paths.byoCertsDir/var/lib/patchpanel/certs/byoBring-your-own PEM uploads (renewed externally).
paths.letsencryptDir/etc/letsencryptCertbot’s account + cert store (unchanged from certbot defaults).
paths.letsencryptLog/var/log/letsencrypt/letsencrypt.logTailed by the live-logs SSE endpoint.
paths.certbotBin/usr/bin/certbotCertbot binary.

Lua plugins

FieldDefaultNotes
paths.luaPluginsDirs[/var/lib/patchpanel/lua-plugins]Whitelist of allowed upload roots. Plugins outside these dirs are rejected.

Internal

FieldDefaultNotes
paths.templatesDir/usr/share/patchpanel/templatesRendering templates (read-only, shipped with the package).
paths.webDir/opt/patchpanel/web/distBuilt React frontend.
paths.webDirDebug/opt/patchpanel/web/dist-debugDevelopment bundle. Served when PATCHPANEL_DEBUG_UI=1;
falls back to webDir if missing.

haproxy.reload.*

How patchpanel reloads HAProxy after a config swap.

FieldTypeDefaultWhat it does
haproxy.reload.methodselectmaster-socketOne of master-socket (zero-downtime), systemctl, child-process.
haproxy.reload.hardStopAfterstring30sOld worker drain deadline before forced termination.
haproxy.reload.validateBeforeReloadbooltrueRun haproxy -c against the rendered cfg before swapping.
haproxy.reload.rollbackOnFailurebooltrueRestore previous cfg + reload if the new cfg fails validation.

renewal.*

Let’s Encrypt renewal scheduler defaults.

FieldTypeDefaultWhat it does
renewal.schedulestring5 8 * * 1,4Cron expression — Monday/Thursday 08:05 by default.
renewal.defaultPropagationSecondsinteger (0–3600)120DNS-01 propagation wait. Cloudflare’s 10s default is too short for ≥20 SAN certs.

auth.strategy

FieldTypeDefaultWhat it does
auth.strategyselectlocalOne of none / ha-ingress / local.
  • local — cookie session (JWT) + Bearer API tokens. Default for Debian baremetal.
  • ha-ingress — trust the HA supervisor proxy IP (listed in server.trustProxy). Users authenticate to Home Assistant upstream; requests through ingress are treated as admin.
  • none — no auth. Dev only — never on a network-exposed deployment. Logs a startup warning.

See the Authentication guide for the full model.

security.*

Cookie/JWT secret + session defaults + bcrypt cost + HTTPS hardening.

FieldTypeDefaultWhat it does
security.jwtSecretpassword__JWT_SECRET_FROM_FILE__ (substituted at install)Session/JWT signing key. The postinst generates 32 random bytes via openssl rand -hex 32, writes them to /etc/patchpanel/.jwt-secret (mode 0600), and substitutes the placeholder. The migrator does the same belt-and-suspenders on first start.
security.jwtExpirystring24hJWT lifetime — 1h, 24h, 7d, 30m, etc.
security.sessionCookieNamestringpatchpanel.sidBrowser session cookie name.
security.sessionSecurebooltrueSend the cookie only over HTTPS.
security.sessionSameSiteselectlaxSameSite policy.
security.bcryptRoundsinteger (10–15)12Cost factor for passwords and API tokens.
security.apiKeyEncryptEnabledboolfalseReserved — patchpanel’s tokens are bcrypt-hashed only; no plaintext-recovery path exists regardless of this flag.
security.csrfEnabledbooltrueLusca CSRF on cookie-authenticated routes. /api/* bypasses CSRF (JSON bodies + Bearer auth model).
security.helmetEnabledbooltrueHelmet middleware (CSP, HSTS, XFO, noSniff, referrerPolicy).
security.hstsEnabledbooltrueStrict-Transport-Security header.
security.hstsMaxAgeinteger31536000HSTS max-age in seconds (1 year).
security.hstsIncludeSubdomainsbooltrueApply HSTS to all subdomains.
security.hstsPreloadboolfalseInclude the preload directive — only enable after submission to.

cors.*

FieldTypeDefaultWhat it does
cors.enabledbooltrueEnable CORS middleware.
cors.whitelistarray[]Allowed origin URLs (exact match). Empty = same-origin only.
cors.credentialsbooltrueSend Access-Control-Allow-Credentials. Required for cookie auth.

rateLimit.*

Tiered rate limits — separate buckets for auth, write, and read traffic.

FieldTypeDefaultWhat it does
rateLimit.authWindowMsinteger900000Window for the auth tier (15 min).
rateLimit.authMaxinteger25Max auth requests per window.
rateLimit.writeWindowMsinteger60000Window for write endpoints.
rateLimit.writeMaxinteger60Max writes per window.
rateLimit.readWindowMsinteger60000Window for read endpoints.
rateLimit.readMaxinteger1000Max reads per window.

logging.*

Backend log level + destination.

FieldTypeDefaultWhat it does
logging.levelselectinfoMinimum level — error/warn/info/debug/trace.
logging.formatselectprettypretty for the HA log viewer / journald; json for log aggregators.
logging.directorystring/var/log/patchpanelFile-rotated log directory. journald is the primary sink.
logging.auditRetentionDaysinteger (30–3650)365Days of audit-log history to retain before vacuuming.

frontendLogging.*

Browser-side logger config. Returned in the /health response and consumed by the React UI’s Logger.js on first call. Edit these to crank verbosity in production browsers without rebuilding the frontend.

FieldTypeDefaultWhat it does
frontendLogging.enabledbooltrueMaster switch. When off, the SPA silences every category
and stops shipping unhandled errors to /api/client-errors.
frontendLogging.levelselectinfoDefault level for every category that has no explicit override below.
Errors are always captured at error level via the ErrorBoundary + window listeners
and shipped to /api/client-errors regardless of this setting (unless enabled is false).
frontendLogging.categories.appselectinfoGeneric UI plumbing (Layout, theme, routing).
frontendLogging.categories.authselectinfoLogin, logout, session probe, token CRUD, setup wizard.
frontendLogging.categories.apiselectinfoAPI client wrappers, OpenAPI viewer, /api-docs page.
frontendLogging.categories.stateselectinfoState document reads/writes, snapshots, raw state.
frontendLogging.categories.haproxyselectinfoRuntime control, stats sockets, server states.
frontendLogging.categories.certselectinfoLet’s Encrypt, BYO certs, trusted CAs, CRLs.
frontendLogging.categories.peerselectinfoCluster sync, peer pairing, keepalived/VRRP.
frontendLogging.categories.errorselectinfoErrorBoundary + window.onerror + unhandledrejection capture. Raising above error disables error capture — leave at info or lower.

geoip.*

GeoIP enrichment for the dashboard origin panels.

FieldTypeDefaultWhat it does
geoip.enabledboolfalseMaster switch.
geoip.dbPathstring/var/lib/patchpanel/geoip/GeoLite2-City.mmdbMaxMind / DB-IP MMDB path.
geoip.fallbackProviderselectnoneHTTP fallback when MMDB lookup misses — none, ip-api, ipinfo.
geoip.updateSchedulestring0 4 * * 0Cron expression for MMDB auto-update. Default weekly Sun 04:00.

Special fields: lifecycle behaviours

The JWT secret (security.jwtSecret)

Generated and substituted twice for belt-and-suspenders:

  1. postinst writes /etc/patchpanel/.jwt-secret via openssl rand -hex 32 (mode 0600, owner patchpanel:patchpanel). Then sed -i substitutes __JWT_SECRET_FROM_FILE__ placeholder in the freshly-copied /etc/patchpanel/config.yaml with the secret.
  2. configMigrator at first daemon start does the same check — if security.jwtSecret.value is empty / the placeholder / contains change-this / contains example, it generates a new secret and rewrites the file.

The sidecar file .jwt-secret exists so external scripts (systemd reload helpers, monitoring) can read the secret without parsing YAML.

The setup token (paths.setupToken)

Generated by postinst on fresh install only (openssl rand -hex 32 > /etc/patchpanel/setup.token, mode 0600). Consumed by POST /api/setup/complete after the operator creates the first admin user. The file is deleted on successful consumption — the setup wizard is single-shot.

The wizard requires both the token file present AND users.json empty. Either alone won’t open the setup flow — prevents stale-token replay and prevents racing the wizard on an installed-but-never-opened deployment.

Recovery if locked out after the token’s been consumed:

sudo patchpanel user-add --username admin2     # create a new admin
sudo patchpanel user-reset --username admin    # reset existing admin's password

Watermark and preservation (config-write.js)

Every save through PUT /api/config prepends a watermark header to the file:

# patchpanel-managed config — written by /api/config
# UI-driven saves rewrite this file; comments do not survive the round-trip.

On the first save, if the existing file does NOT carry the watermark, writeRawConfig copies it verbatim to <configPath>.preserved-<iso> first. Operators who hand-edited a config and then used the UI find their original at /etc/patchpanel/config.yaml.preserved-2026-05-17T12-34-56Z. Subsequent saves don’t re-preserve.

The migrator emits its own watermark header on fresh installs and version upgrades — so the first UI save against a migrator-written config doesn’t create a redundant .preserved-* sidecar.

Migration on version upgrade

configMigrator runs at every daemon start (server/src/config/configMigrator.js). It diffs config.version against package.json.version and:

  • up_to_date — versions match, no-op.
  • fresh_install — no existing config; writes the template, JWT secret, version stamp.
  • version_mismatch — runs jsonMerger.mergeFiles([template, userConfig]) so new template keys appear and your .value edits survive. Pre-merge, a timestamped backup is written to <configPath>.backup.<ISO-timestamp>.

The migrator is not a data migrator — it doesn’t transform your values across versions. New template fields simply appear with their defaults; removed template fields linger in the user config until manually cleaned.

If a dev override exists (<repo>/dev.config.yaml), the migrator becomes a no-op (you’re presumed to be hacking on patchpanel and don’t want background rewrites).

Environment variables

The daemon respects:

VarSet byPurpose
CONFIG_PATHsystemd unit, HA addon run.shOverride the config-file lookup.
NODE_ENVsystemd unit (production)Conventional Node lib gate.
NODE_OPTIONS=--use-openssl-casystemd unitTrust the system CA store for outbound HTTPS (corporate CAs, etc.).
PATCHPANEL_DEBUG_UIHA addon when debug_ui: trueServe the development React bundle (paths.webDirDebug)
instead of the production build.
SUPERVISOR_TOKENHA supervisorHA API token for callbacks. Name configurable via server.supervisorTokenEnv.

Troubleshooting

“Cannot read config”

configLoader throws if none of the candidate paths exist. Check:

sudo ls -l /etc/patchpanel/config.yaml
sudo journalctl -u patchpanel -n 50

If the file is missing, reinstall the package (sudo apt install --reinstall patchpanel) — the postinst re-seeds from the template only when the file is absent.

“haproxy -c failed: …”

Your state document rendered an invalid HAProxy config. The state apply pipeline catches this and rolls back automatically — no half-applied state. Check the HAProxy stderr in the API error response, the audit log (GET /api/audit?category=state), or journalctl -u patchpanel.

Setup token regeneration

If you lost the setup token but the wizard hasn’t run yet (no users):

openssl rand -hex 32 | sudo tee /etc/patchpanel/setup.token
sudo chown patchpanel:patchpanel /etc/patchpanel/setup.token
sudo chmod 600 /etc/patchpanel/setup.token

Open https://<host>:8099/setup-admin?token=$(sudo cat /etc/patchpanel/setup.token).

Reverting an unwanted config change

Backups live at /etc/patchpanel/config.yaml.backup.<ISO-timestamp> (migrator) or /etc/patchpanel/config.yaml.preserved-<iso> (first-save UI). Copy one back and systemctl restart patchpanel.

See also