Access & Authentication

ProxMenux Monitor~15 min

Reaching the dashboard, the first-launch security flow, and every layer that can sit between an attacker and the host: password + TOTP, JWT sessions, long-lived API tokens, HTTPS, reverse proxies, Secure Gateway, and the optional Fail2Ban jail.

Authentication is opt-in

On first launch the dashboard shows a single dialog — "Protect Your Dashboard?" — with two buttons: Yes, Setup Password and No, Continue Without Protection. Saying no leaves every API endpoint open on TCP 8008 — fine for an isolated lab LAN, dangerous on anything else. Two-Factor Authentication (2FA) is not part of this initial choice; it's configured later from the Security tab once a password is set.

Reaching the dashboard

ProxMenux Monitor binds to 0.0.0.0:8008. There are three common ways to reach it:

# 1) Direct on the LAN
http://<proxmox-ip>:8008

# 2) Behind a reverse proxy with a dedicated host name (recommended off-LAN)
https://monitor.example.com

# 3) Through Secure Gateway (Tailscale) — same LAN URL, from anywhere
http://<proxmox-lan-ip>:8008      # works from any device on your tailnet

Direct access matches what the systemd unit ships out of the box. The reverse-proxy and Secure Gateway sections below cover the other two. The Monitor honours X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host so URLs and CORS work behind any of them without manual configuration.

First-launch flow

The first time you open the dashboard, the frontend calls GET /api/auth/status. If the auth config has never been written (configured: false), a single dialog appears titled "Protect Your Dashboard?" with two choices:

First-launch dialog 'Protect Your Dashboard?' with two buttons: Yes Setup Password, No Continue Without Protection
The first-launch authentication chooser. Two buttons — password protection or skip. Re-runs after a fresh install or after "Disable authentication" from Settings.
ButtonWhat happensAPI call
Yes, Setup PasswordOpens a form with the mandatory username + password and an optional display name + avatar image. Stores them in auth.json with enabled: true. Returns a JWT so you're logged in immediately. The form is documented in detail below.POST /api/auth/setup
No, Continue Without ProtectionMarks declined: true in auth.json. Every API endpoint is publicly accessible until you change your mind from Settings.POST /api/auth/skip

2FA is configured later, not here

The first-launch dialog covers only the password decision. Two-Factor Authentication (TOTP) is set up afterwards from the Security tab once you're logged in with a password. The full TOTP walkthrough is further down this page.

Creating the first user

Clicking Yes, Setup Password opens a single form that creates the account and, optionally, seeds the user's profile in one go so the avatar appears in the header right after saving. The fields are:

FieldRequiredNotes
UsernameYesThe login identifier. Cannot be changed later from the UI; editing it requires touching auth.json directly.
PasswordYesMinimum 10 characters, with at least 3 of the 4 categories (lowercase, uppercase, digit, symbol). A short list of obvious passwords (password, 12345678, proxmenux…) is rejected outright. The same rules are enforced server-side, so a curl call cannot bypass the front-end check.
Display nameNoFriendly label shown in the header dropdown and on the profile page. Falls back to the username when empty. Can be changed later from Avatar → View profile.
Avatar imageNoPNG, JPEG, WebP or GIF up to 2 MB. Rendered as a circle in the header and on the profile page. When empty, the header shows the first letter of the display name (or username) on a coloured circle. Can be uploaded, replaced or removed later from the profile page.
Create-user form with mandatory Username + Password fields and the optional Display name + Avatar upload section
The first-launch create-user form. Display name and avatar are optional — leaving them empty creates the account and falls back to a single-letter circle in the header.

One save, three API calls under the hood

The form submits POST /api/auth/setup first (username + password). On success it uses the freshly-issued JWT to follow up with PUT /api/auth/profile (display name) and POST /api/auth/profile/avatar (avatar bytes) if those fields were filled. Failures on the profile calls are non-fatal — the account is already created and you can finish the profile later from the dedicated page.

Avatar menu and profile page

Once authentication is configured, an avatar circle appears at the top-right of every dashboard page next to the theme toggle. Clicking it opens a small dropdown with shortcuts to the profile page and the Security tab, plus a Sign out action — closing the session can be done from here or from the Security tab, whichever is closer to where you are.

The profile page itself is a small card with an avatar preview, the username (read-only), and the display name with an inline edit button. Avatar uploads, replacements and removals are atomic — the header avatar refreshes automatically when any of them succeed, so there is no need to reload the page. The same set of endpoints documented in the next section are used by both the create-user form and the profile page.

Profile page with avatar preview circle, Upload / Replace / Remove buttons, read-only username and editable display name field
The dedicated profile page. Username is read-only; display name and avatar can be edited from here without touching the Security tab.
EndpointWhat it does
GET /api/auth/profileReturns the current username, display name and whether an avatar is set (has_avatar, avatar_mtime).
PUT /api/auth/profileUpdates the display name. Body: { "display_name": "..." }.
GET /api/auth/profile/avatarReturns the avatar bytes (PNG / JPEG / WebP / GIF) with the matching content type. Requires the Bearer header — the front-end fetches it as a blob and converts to a local object URL for rendering.
POST /api/auth/profile/avatarUploads a new avatar (max 2 MB). Content type must match the file. Old avatar is replaced atomically.
DELETE /api/auth/profile/avatarRemoves the avatar. The header falls back to the initial-on-coloured-circle placeholder.

Continuing without protection is reversible — but only from the host

Once you click No, Continue Without Protection, the welcome dialog never appears again. You can re-enable authentication from the Security tab inside the dashboard, or by editing /root/.config/proxmenux-monitor/auth.json and removing the declined flag, then restarting the service.

Password authentication

After Set up, every API call (except the few public endpoints listed below) requires a JWT in Authorization: Bearer &lt;token&gt;:

  • Session token (login): 24-hour expiration. Issued by POST /api/auth/login.
  • API token (integrations): 365-day expiration. Issued by POST /api/auth/generate-api-token. Documented separately in the next section.
Login screen shown after authentication is configured — username and password fields
Once authentication is configured, every visit to the dashboard starts here. With 2FA enabled, the screen asks for the 6-digit code in a second step after the password is accepted.

Login flow

# Without 2FA
curl -X POST http://<host>:8008/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"<user>","password":"<password>"}'

# Response
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

With 2FA enabled, the same call returns requires_totp: true first. Re-issue with the 6-digit code:

curl -X POST http://<host>:8008/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"<user>","password":"<password>","totp_token":"123456"}'

Public endpoints (no token)

These are the only endpoints that work without authentication, even when auth is enabled:

  • /api/auth/login, /api/auth/status, /api/auth/setup — the auth flow itself, by necessity.
  • /api/system-info — lightweight system snapshot (hostname, uptime, health.status). The right endpoint for external probes (Uptime Kuma, load-balancer health checks, status pages).

Cryptography and storage

ProxMenux Monitor is open source — none of this is secret. Documenting the stack here explicitly is a deliberate choice: operators who store credentials on their host deserve to know how those credentials are protected before they decide to trust them. The algorithms below are the same ones the code in scripts/auth_manager.py uses; this section is a contract, not a marketing promise.

AssetAlgorithmWhere it lives
PasswordPBKDF2-HMAC-SHA256 with a per-password random salt and a high iteration count (OWASP 2023+ baseline). Stored as pbkdf2_sha256$&lt;iters&gt;$&lt;salt&gt;$&lt;hash&gt;.auth.jsonpassword_hash
Session / API JWTHS256 signed with a per-install secret minted at first launch (secrets.token_urlsafe, ≥48 bytes). Tokens carry iss=proxmenux-monitor + aud=api claims; the signature is validated against the current secret on every request.Secret: auth.jsonjwt_secret. JWT itself: only on the client.
API token metadataSHA-256 of the JWT stored alongside a signed_with fingerprint of the jwt_secret used to mint it — used to display the token in the UI and to detect tokens whose signing secret has been rotated.auth.jsonapi_tokens[]
2FA TOTP secretStandard TOTP (RFC 6238) base32-encoded. Backup codes are pre-generated, single-use, hashed with the same PBKDF2 scheme as the password.auth.jsontotp_secret + backup_codes[]
RevocationsWhen a token or session is revoked, its SHA-256 is added to a deny-list checked on every verification (mem-cached for ~30 s to avoid disk reads on the hot path).auth.jsonrevoked_tokens[]

auth.json — what it contains and how it's protected

Everything ProxMenux Monitor needs to authenticate you lives in a single file: /root/.config/proxmenux-monitor/auth.json, mode 0600, owner root. The file holds hashes (PBKDF2) and signing material (jwt_secret, totp_secret) — never a plaintext password. Treat it like any other root-only secret: if you back up or replicate the host, encrypt the destination, and never commit it to version control.

Rotating jwt_secret invalidates all existing JWTs

If auth.json is regenerated (manual delete, reinstall, restore from a backup with a different secret) the jwt_secret changes and every previously-issued JWT — both interactive sessions and long-lived API tokens — fails verification with "Invalid or expired token". The UI flags affected API tokens with an Invalid — regenerate badge so the operator knows to revoke and re-mint them; Home Assistant / scripts / any external client needs a fresh token after that.

Recovering a lost password

There is no online "forgot password" flow — by design, since the dashboard runs on the operator's own host and the recovery path is shell access to that host. ProxMenux ships a guided reset inside the configuration menu so you don't have to hand-edit auth.json:

# 1. Run the ProxMenux menu as root
menu

# 2. Settings → Reset ProxMenux Monitor Password
#    The menu will:
#     - Back up auth.json to auth.json.bak-<UTC timestamp>
#     - Stop the proxmenux-monitor service
#     - Clear username / password_hash / TOTP secret / backup codes
#     - Keep jwt_secret and api_tokens intact
#     - Restart the service

# 3. Open the dashboard at http://<host>:8008
#    The setup wizard appears — create a new admin account.

What survives the reset

Only the interactive login is wiped. The jwt_secret and the registered api_tokens are preserved — so Home Assistant and any other script using a long-lived API token continue to work without reconfiguration. If you want a fully clean slate (also rotate the JWT secret), delete auth.json manually and restart the service. The next launch generates a fresh secret and all old tokens become invalid.

Physical-access prerequisite

This reset path needs root shell on the host. That is the trust anchor of the whole authentication scheme: anyone who can run menu as root can already do anything on the box, so giving them password reset is not a privilege increase. The corollary: if you let an untrusted user reach the Proxmox shell, the Monitor login won't protect anything that user couldn't already destroy by other means.

Two-Factor Authentication (TOTP)

2FA adds a second factor on top of your password: a 6-digit code that rotates every 30 seconds, generated on a phone or password manager you control. Even if someone obtains the password, they still can't log in without the code from your device. ProxMenux Monitor implements the standard TOTP protocol (RFC 6238), so any authenticator app works.

Pick an authenticator app

If you already use one for Google / GitHub / your bank, that one will work — skip to the setup walkthrough. If not, here's a survey of common options. All of them are free; the differences are mainly about which platforms they run on and how (or whether) they back up your secrets.

AppPlatformsNotes
Google AuthenticatoriOS, AndroidThe default for many users. Optional Google-account cloud backup.
Microsoft AuthenticatoriOS, AndroidMicrosoft-account backup. Also handles MS push notifications if you use them at work.
AuthyiOS, Android, desktopMulti-device encrypted sync (the desktop app is being phased out — check the latest status).
Apple PasswordsiOS, iPadOS, macOS, visionOS, Windows (via iCloud)Built into Apple OSes; standalone Passwords app since iOS 18 / macOS Sequoia. Stores TOTP next to the password and syncs across devices via iCloud Keychain.
BitwardeniOS, Android, desktop, browserOpen source. TOTP lives next to the password it protects (handy if you also use BW for passwords; defeats "separate device" if you don't).
1PasswordiOS, Android, desktop, browserSame idea as Bitwarden — TOTP integrated with the password vault. Subscription.
Aegis AuthenticatorAndroidOpen source. Encrypted on-device backup file you control. No cloud, no account required.
Raivo OTPiOS, macOSOpen source. Optional iCloud sync. The Apple-ecosystem counterpart to Aegis.
Ente AuthiOS, Android, desktop, webOpen source. End-to-end encrypted cloud sync across devices.
2FASiOS, Android, browser extensionOpen source. Optional encrypted cloud backup; browser extension can autofill codes.
FreeOTP+Android, iOSOpen source (Red Hat-led). Minimal — no cloud, no account.

What "backup" really matters for

If you lose the device that has the authenticator on it, the only ways back in are (1) a backup code saved when you enabled 2FA, or (2) a backup of the authenticator's vault. Apps with cloud sync (Google Auth, Microsoft Auth, Authy, Apple Passwords, Ente, 2FAS, Bitwarden, 1Password) can restore on a new device. Apps without cloud (Aegis, Raivo, FreeOTP+) need an encrypted export file you've copied somewhere safe. Either approach works — the bad case is "no backup at all".

Step-by-step setup from the dashboard

2FA setup screen with QR code and backup codes
The 2FA setup dialog — QR code, Base32 secret (for manual entry), and the ten one-time backup codes. The codes are only displayed here; if you close the dialog without copying them, they're gone.
  1. Install the authenticator app on your phone (or open your password manager). One of the apps from the table above. You only need to do this once — the same app will hold codes for every service you protect.
  2. Log into the dashboard with your username and password.
  3. Open the Security tab in the dashboard sidebar, then click Enable 2FA. A dialog opens with a QR code, a long string in Base32 format and ten short codes labelled "backup codes".
  4. Add the entry to the authenticator app:
    • Easy path: in the app, tap Add accountScan QR code, point the camera at the QR on the screen. The app names the entry automatically (something like ProxMenux Monitor (your-username)) and starts showing a 6-digit code that refreshes every 30 seconds.
    • Manual fallback (when scanning isn't possible — e.g. setting up on the same phone you opened the dashboard with): tap Add accountEnter setup key. Type any name (e.g. Proxmox Monitor), paste the Base32 string from the dialog, leave Type as Time-based, save.
  5. Save the backup codes. Copy the ten codes somewhere safe — a password manager, an encrypted note, a printed copy in a drawer. Treat them like spare keys: each works exactly once and gets you in if your phone is gone or broken.
  6. Confirm by typing the current 6-digit code from the app into the "Verification code" field of the setup dialog and submit. Codes refresh every 30 seconds, so if it expires while you're typing, just enter the next one.
  7. Done. 2FA is now active. Next time you log in, the dashboard asks for the password first; once it's accepted it asks for the current 6-digit code.

Test before logging out

Once you click Save, log out and log back in immediately, while the setup dialog is still fresh in your mind. If the code is rejected (clock-skew between server and phone is the most common cause), you can still fix it from the open session. Logging out without testing first means a one-trip-no-return — at that point only a backup code or editing auth.json on the host gets you back in.

Lost authenticator

Three escape hatches, in order of how disruptive they are:

  • Use a backup code. At the login screen, in the TOTP field, type one of the ten codes you saved at setup time. Each works once and is then consumed; the remaining codes still work. Once you're in, regenerate 2FA from Settings to get a fresh ten.
  • Restore the authenticator from cloud / backup. If your app has a cloud sync (Google, Microsoft, Authy, Apple Passwords via iCloud Keychain, Ente, 2FAS) install it on a new device, sign in, and the entries reappear. If your app uses an encrypted export file (Aegis, Raivo, FreeOTP+), install the app on the new device and import the file.
  • Disable 2FA from the host shell. When the previous options aren't available, edit /root/.config/proxmenux-monitor/auth.json on the Proxmox host (you need root SSH or console access), set totp_enabled to false, save, and restart the service:
    systemctl restart proxmenux-monitor.service

You can log in with username + password only, then re-enable 2FA from Settings.

Disable 2FA

From the dashboard, open the Security tab and click Disable 2FA. The endpoint POST /api/auth/totp/disable requires the current password as confirmation, then deletes the TOTP secret and clears the backup codes. Remember to also remove the entry in the authenticator app — the app doesn't know the server side is gone, so the dead entry will sit there forever otherwise.

The 6-digit code is always rejected

TOTP is time-based — server clock and phone clock must agree to within ~30 s. Two checks:
  • Phone: Settings → Date & Time → automatic / network sync ON.
  • Proxmox host: timedatectl status — "System clock synchronized: yes" should be visible. If not, timedatectl set-ntp true and wait a minute.
Once both clocks agree, the code is accepted within the next 30-second window.

API tokens (long-lived)

Browser sessions expire after 24 hours. For unattended integrations (Homepage widgets, Home Assistant sensors, Grafana scrapers, Uptime Kuma probes…) you generate a separate API token that lives 365 days. The token is a JWT signed with the same secret as the session token, but its token_name claim makes it easy to track and revoke individually.

API tokens panel showing the token list with name, prefix, created date and expiry
The API tokens list under Settings — name, prefix (last 4 chars are shown for identification), created and expiry dates, revoke action.

Generate a token

From the dashboard:

  1. Navigate to the Security tab → API Access Tokens section.
  2. Type a descriptive name (e.g. "Home Assistant").
  3. Re-enter your password. If 2FA is on, also the current 6-digit code.
  4. Click Generate Token. The token appears once — copy it immediately.

From the command line:

curl -X POST http://<host>:8008/api/auth/generate-api-token \
  -H "Authorization: Bearer <session-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "password": "<your-password>",
    "totp_token": "123456",
    "token_name": "Home Assistant"
  }'

# Response — the "token" field is the only place the token appears.
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_name": "Home Assistant",
  "expires_in": "365 days"
}

Use a token

curl -H "Authorization: Bearer <api-token>" \
  http://<host>:8008/api/system

Revoke a token

From the panel above: each row has a Revoke action that adds the token hash to revoked_tokens in auth.json. Revoked tokens fail validation immediately on the next request.

# Same operation via API
curl -X DELETE http://<host>:8008/api/auth/api-tokens/<token-id> \
  -H "Authorization: Bearer <session-token>"

Token security cheat-sheet

  • Store tokens in your integration's native secrets store — Homepage secrets.yaml, Home Assistant !secret, environment variables, etc. Never commit them to git.
  • One token per integration, named after the consumer. Revoke individually when retiring an integration.
  • Rotate every 6–12 months. The expiry is a hard limit, not a recommendation.

Full storage best-practices and integration recipes live in API Reference → Token Management and Integrations.

HTTPS

Two paths to TLS:

  1. Reverse proxy (recommended). Terminate TLS on Nginx / Caddy / Traefik and forward HTTP on port 8008 to the Flask process. Snippets below.
  2. Direct HTTPS in the AppImage. Configure a certificate via POST /api/ssl/configure (UI: Settings → SSL). When SSL is configured the process switches from Flask's dev server to gevent.pywsgi with the gevent-websocket handler so WebSocket terminal also works over WSS. The cert files live wherever you point them; the paths are stored in the SSL config.

Direct HTTPS limitations

The bundled gevent path is suitable for self-signed or LAN-only certificates. For Let's Encrypt / ACME and automatic renewal, run a real reverse proxy in front — Caddy auto-renews and Traefik / Nginx have well-known patterns. The Monitor doesn't implement ACME on its own.

Secure Gateway (Tailscale)

Reverse proxies are the classic answer to "reach the dashboard from outside" but they require a public domain, certificate, and an open port on the edge. Secure Gateway is the zero-port alternative shipped inside the Monitor itself — a pre-built deployable app that spins up an Alpine LXC running Tailscale as a subnet router. Once joined to your tailnet, every device on it can hit the Monitor at the host's own LAN IP — from a laptop on holiday, a phone on 5G, or another node — without exposing TCP 8008 to the internet.

Why this is convenient

The URL stays the same as on the LAN — http://&lt;proxmox-lan-ip&gt;:8008 works everywhere Tailscale works. No certificates, no DNS, no port forwarding. The Monitor itself sees the request as coming from a tailnet IP (typically 100.x.y.z), so the auth log and the Fail2Ban hook still function as on the LAN.

The deploy flow is one screen — pick the host LXC storage, paste a Tailscale auth-key (generated at login.tailscale.com/admin/settings/keys), choose which subnets to advertise, click Deploy. The LXC takes ~30 seconds to bootstrap and registers in the tailnet automatically.

Step-by-step deployment, subnet-routes configuration, Tailscale ACLs and Exit Node mode are documented separately in Dashboard → Security → Secure Gateway — that's where the deploy wizard lives in the dashboard UI. This page only covers the access pattern.

Reverse proxy snippets

The simplest layout is a dedicated host name for the Monitor (e.g. monitor.example.com) pointing at port 8008 on the Proxmox host. Snippets below use that pattern. Sub-path mounts (example.com/proxmenux-monitor/) are possible but require extra rewriting and are not the default — see the callout at the end.

Nginx

# /etc/nginx/sites-available/proxmenux-monitor.conf
server {
    listen 443 ssl http2;
    server_name monitor.example.com;

    ssl_certificate     /etc/letsencrypt/live/monitor.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/monitor.example.com/privkey.pem;

    location / {
        proxy_pass         http://127.0.0.1:8008;
        proxy_http_version 1.1;

        # WebSocket upgrade (terminal tab)
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        "upgrade";

        # Real client IP — required for the auth log + Fail2Ban hook
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Forwarded-Host  $host;

        # Long-running terminal sessions
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Caddy

# Caddyfile
monitor.example.com {
    reverse_proxy 127.0.0.1:8008 {
        # Caddy auto-handles WebSocket upgrades and forwards X-Forwarded-* by default.
        header_up Host {host}
        header_up X-Real-IP {remote}
    }
}

Traefik (labels — Docker / Kubernetes)

# docker-compose snippet, or equivalent IngressRoute on Kubernetes
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.proxmenux.rule=Host(`monitor.example.com`)"
  - "traefik.http.routers.proxmenux.tls=true"
  - "traefik.http.routers.proxmenux.tls.certresolver=letsencrypt"
  - "traefik.http.services.proxmenux.loadbalancer.server.port=8008"
  # WebSocket and forwarded headers are on by default in Traefik.

Advanced: sub-path mounts under an existing domain

If you don't want a dedicated host name, you can mount the Monitor under a path on an existing domain — for example example.com/proxmenux-monitor/. The Next.js build uses relative asset paths so static files resolve, but the proxy must strip the prefix before forwarding so the Monitor still receives plain /api/* URLs. On Nginx that's a location /proxmenux-monitor/ &rbrace; proxy_pass http://127.0.0.1:8008/; &lbrace; (the trailing slash on proxy_pass does the strip). On Caddy, use handle_path /proxmenux-monitor/*. A dedicated host name is simpler.

Audit log

Every authentication event (success and failure) is appended to /var/log/proxmenux-auth.log in a single line, syslog-style format:

# Failed login from 192.0.2.10 (real IP recovered from X-Forwarded-For)
2026-04-24 14:32:11 WARNING proxmenux.auth: authentication failure; rhost=192.0.2.10 user=admin

# Successful login
2026-04-24 14:32:18 INFO    proxmenux.auth: authentication success;  rhost=192.0.2.10 user=admin

Tail it the usual way: tail -F /var/log/proxmenux-auth.log. The file is rotated by logrotate if a config drop-in is added; the Monitor itself does not rotate it.

Optional: Fail2Ban jail

Fail2Ban is not bundled with the Monitor

Fail2Ban is not installed by ProxMenux Monitor itself. Install it via Security → Fail2Ban in the ProxMenux menu (or with the standard Debian package). Without it, the Monitor still writes the audit log above — it just doesn't auto-ban repeat offenders.

When Fail2Ban is installed, the ProxMenux integration ships a [proxmenux] jail that:

  • Reads /var/log/proxmenux-auth.log.
  • Matches the authentication failure; rhost=&lt;ip&gt; pattern with a dedicated filter.
  • Bans the offending IP at the kernel firewall level by default.
  • Is queried by the Flask before_request hook every 30 s — so even when the firewall can't block (because the connection comes from the reverse proxy), the application returns HTTP 403 to banned IPs based on what Fail2Ban knows.

Configuration, ban time tuning and unban procedures are in Security → Fail2Ban.

Troubleshooting

The first-launch screen never appears

Either auth is already configured (configured: true) or somebody already chose Skip. To start fresh:
rm /root/.config/proxmenux-monitor/auth.json
systemctl restart proxmenux-monitor.service
This wipes the auth state — also any TOTP secrets and API tokens. Back up auth.json first if you have tokens you want to keep.

HTTP 401 on every request from a working API token

Token expired (365 d limit) or got into the revoked_tokens list. Generate a new one in Settings and update the integration. To check:
curl -H "Authorization: Bearer <token>" \
  http://<host>:8008/api/system | jq .
Expired or revoked tokens return {"error":"Invalid or expired token"}.

Can't log in after enabling 2FA, no authenticator at hand

Use a backup code in the TOTP field. If those are gone, edit /root/.config/proxmenux-monitor/auth.json from a host shell, set totp_enabled to false, restart the service.

Reverse proxy works but the terminal tab disconnects every minute

WebSocket idle timeout in the proxy. Bump the read timeout (Nginx: proxy_read_timeout 86400s; Traefik: idleTimeout in the entry-point or middleware) and confirm proxy_set_header Upgrade $http_upgrade and Connection "upgrade" are present.

Where to next