mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
397 lines
34 KiB
JSON
397 lines
34 KiB
JSON
{
|
||
"meta": {
|
||
"title": "Proxmox Dashboard Authentication — 2FA, API Tokens, User Profile, Reverse Proxy | ProxMenux Monitor",
|
||
"description": "Reach and secure ProxMenux Monitor: first-launch protect-your-dashboard flow, password authentication with display name + avatar, profile page, TOTP 2FA enrolment, long-lived API tokens for scripts, HTTPS configuration, reverse-proxy snippets for Nginx, Caddy and Traefik, the audit log and the optional Fail2Ban jail.",
|
||
"ogTitle": "Proxmox Dashboard Authentication — 2FA, API Tokens, Reverse Proxy",
|
||
"ogDescription": "Secure ProxMenux Monitor with password + TOTP 2FA, long-lived API tokens, HTTPS, reverse-proxy snippets and an optional Fail2Ban jail.",
|
||
"twitterTitle": "Proxmox Dashboard Authentication | ProxMenux Monitor",
|
||
"twitterDescription": "Password + TOTP 2FA, API tokens, HTTPS, Nginx/Caddy/Traefik snippets and audit log."
|
||
},
|
||
"header": {
|
||
"title": "Access & Authentication",
|
||
"description": "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.",
|
||
"section": "ProxMenux Monitor"
|
||
},
|
||
"intro": {
|
||
"title": "Authentication is opt-in",
|
||
"body": "On first launch the dashboard shows a single dialog — <em>\"Protect Your Dashboard?\"</em> — with two buttons: <strong>Yes, Setup Password</strong> and <strong>No, Continue Without Protection</strong>. 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 <strong>not</strong> part of this initial choice; it's configured later from <strong>the Security tab</strong> once a password is set."
|
||
},
|
||
"reaching": {
|
||
"heading": "Reaching the dashboard",
|
||
"intro": "ProxMenux Monitor binds to <code>0.0.0.0:8008</code>. There are three common ways to reach it:",
|
||
"outro": "Direct access matches the default systemd unit. Reverse-proxy and Secure Gateway access are described below. Trust for client IPs in <code>X-Forwarded-For</code> requires PROXMENUX_TRUST_PROXY=1 and a proxy you control. The presence of <code>X-Forwarded-Proto</code> or <code>X-Forwarded-Host</code> alone does not enable proxy trust. On ports 80/443 the frontend uses relative <code>/api/*</code> URLs; on any other port it calls port 8008 on the same host name."
|
||
},
|
||
"firstLaunch": {
|
||
"heading": "First-launch flow",
|
||
"intro": "The first time you open the dashboard, the frontend calls <code>GET /api/auth/status</code>. If the auth config has never been written (<code>configured: false</code>), a single dialog appears titled <em>\"Protect Your Dashboard?\"</em> with two choices:",
|
||
"imageAlt": "First-launch dialog 'Protect Your Dashboard?' with two buttons: Yes Setup Password, No Continue Without Protection",
|
||
"imageCaption": "The first-launch authentication chooser. Two buttons — password protection or skip. Re-runs after a fresh install or after \"Disable authentication\" from Settings.",
|
||
"headerButton": "Button",
|
||
"headerWhat": "What happens",
|
||
"headerApi": "API call",
|
||
"rows": [
|
||
{
|
||
"button": "Yes, Setup Password",
|
||
"what": "Opens a form requiring a username and password, with optional <em>display name</em> and <em>avatar image</em>. Enables authentication in <code>auth.json</code> with <code>enabled: true</code> and returns a JWT to log you in. Optional profile fields are saved by subsequent calls, as documented below.",
|
||
"api": "POST /api/auth/setup"
|
||
},
|
||
{
|
||
"button": "No, Continue Without Protection",
|
||
"what": "Marks <code>declined: true</code> in <code>auth.json</code>. Every API endpoint is publicly accessible until you change your mind from Settings.",
|
||
"api": "POST /api/auth/skip"
|
||
}
|
||
],
|
||
"twofaCalloutTitle": "2FA is configured later, not here",
|
||
"twofaCalloutBody": "The first-launch dialog covers only the password decision. <strong>Two-Factor Authentication (TOTP)</strong> is set up afterwards from <strong>the Security tab</strong> once you're logged in with a password. The full TOTP walkthrough is further down this page.",
|
||
"createTitle": "Creating the first user",
|
||
"createIntro": "Clicking <em>Yes, Setup Password</em> 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:",
|
||
"headerField": "Field",
|
||
"headerRequired": "Required",
|
||
"headerNotes": "Notes",
|
||
"fieldRows": [
|
||
{
|
||
"field": "Username",
|
||
"required": "Yes",
|
||
"notes": "The login identifier. Cannot be changed later from the UI; editing it requires touching <code>auth.json</code> directly."
|
||
},
|
||
{
|
||
"field": "Password",
|
||
"required": "Yes",
|
||
"notes": "Minimum 10 characters, with at least 3 of the 4 categories (lowercase, uppercase, digit, symbol). A short list of obvious passwords (<code>password</code>, <code>12345678</code>, <code>proxmenux</code>…) is rejected outright. The same rules are enforced server-side, so a curl call cannot bypass the front-end check."
|
||
},
|
||
{
|
||
"field": "Display name",
|
||
"required": "No",
|
||
"notes": "Friendly label shown in the header dropdown and on the profile page. Falls back to the username when empty. Can be changed later from <strong>Avatar → View profile</strong>."
|
||
},
|
||
{
|
||
"field": "Avatar image",
|
||
"required": "No",
|
||
"notes": "PNG, 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."
|
||
}
|
||
],
|
||
"createImageAlt": "Create-user form with mandatory Username + Password fields and the optional Display name + Avatar upload section",
|
||
"createImageCaption": "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.",
|
||
"saveCalloutTitle": "One save, three API calls under the hood",
|
||
"saveCalloutBody": "The form submits <code>POST /api/auth/setup</code> first (username + password). On success it uses the freshly-issued JWT to follow up with <code>PUT /api/auth/profile</code> (display name) and <code>POST /api/auth/profile/avatar</code> (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.",
|
||
"avatarTitle": "Avatar menu and profile page",
|
||
"avatarBody1": "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 <strong>Sign out</strong> action — closing the session can be done from here or from the Security tab, whichever is closer to where you are.",
|
||
"avatarBody2": "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.",
|
||
"profileImageAlt": "Profile page with avatar preview circle, Upload / Replace / Remove buttons, read-only username and editable display name field",
|
||
"profileImageCaption": "The dedicated profile page. Username is read-only; display name and avatar can be edited from here without touching the Security tab.",
|
||
"headerEndpoint": "Endpoint",
|
||
"headerEpWhat": "What it does",
|
||
"endpointRows": [
|
||
{
|
||
"endpoint": "GET /api/auth/profile",
|
||
"what": "Returns the current username, display name and whether an avatar is set (<code>has_avatar</code>, <code>avatar_mtime</code>)."
|
||
},
|
||
{
|
||
"endpoint": "PUT /api/auth/profile",
|
||
"what": "Updates the display name. Body: <code>'{' \"display_name\": \"...\" '}'</code>."
|
||
},
|
||
{
|
||
"endpoint": "GET /api/auth/profile/avatar",
|
||
"what": "Returns 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."
|
||
},
|
||
{
|
||
"endpoint": "POST /api/auth/profile/avatar",
|
||
"what": "Uploads a new avatar (max 2 MB). Content type must match the file. Old avatar is replaced atomically."
|
||
},
|
||
{
|
||
"endpoint": "DELETE /api/auth/profile/avatar",
|
||
"what": "Removes the avatar. The header falls back to the initial-on-coloured-circle placeholder."
|
||
}
|
||
],
|
||
"reversibleTitle": "Continuing without protection is reversible",
|
||
"reversibleBody": "Once you click <em>No, Continue Without Protection</em>, the welcome dialog never appears again. You can re-enable authentication from <strong>the Security tab</strong> inside the dashboard, or by editing <code>/root/.config/proxmenux-monitor/auth.json</code> and removing the <code>declined</code> flag, then restarting the service."
|
||
},
|
||
"password": {
|
||
"heading": "Password authentication",
|
||
"intro": "After setup, protected API calls require a JWT in <code>Authorization: Bearer <token></code>:",
|
||
"items": [
|
||
"<strong>Session token (login):</strong> 24-hour expiration. Issued by <code>POST /api/auth/login</code>.",
|
||
"<strong>API token (integrations):</strong> 365-day expiration. Issued by <code>POST /api/auth/generate-api-token</code>. Documented separately in the next section."
|
||
],
|
||
"loginImageAlt": "Login screen shown after authentication is configured — username and password fields",
|
||
"loginImageCaption": "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.",
|
||
"loginFlowTitle": "Login flow",
|
||
"twofaIntro": "With 2FA enabled, the same call returns <code>requires_totp: true</code> first. Re-issue with the 6-digit code:",
|
||
"publicTitle": "Public endpoints (no token)",
|
||
"publicIntro": "Login, authentication status, initial setup and the lightweight <code>/api/system-info</code> endpoint are public:",
|
||
"publicItems": [
|
||
"<code>/api/auth/login</code> and <code>/api/auth/status</code> are public. <code>/api/auth/setup</code> creates the initial account and rejects attempts to overwrite existing credentials.",
|
||
"<code>/api/system-info</code> is a public lightweight system-information endpoint, including <code>health.status</code>. It is not decorated with the token requirement. /api/auth/status is another public endpoint suitable for a reachability check."
|
||
],
|
||
"cryptoTitle": "Cryptography and storage",
|
||
"cryptoIntro": "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 <code>scripts/auth_manager.py</code> uses; this section is a contract, not a marketing promise.",
|
||
"headerAsset": "Asset",
|
||
"headerAlgo": "Algorithm",
|
||
"headerWhere": "Where it lives",
|
||
"cryptoRows": [
|
||
{
|
||
"asset": "Password",
|
||
"algorithm": "PBKDF2-HMAC-SHA256 with a per-password random salt and a high iteration count (OWASP 2023+ baseline). Stored as <code>pbkdf2_sha256$<iters>$<salt>$<hash></code>.",
|
||
"where": "<code>auth.json</code> → <code>password_hash</code>"
|
||
},
|
||
{
|
||
"asset": "Session / API JWT",
|
||
"algorithm": "HS256 signed with a per-install secret minted at first launch (<code>secrets.token_urlsafe</code>, ≥48 bytes). Tokens carry <code>iss=proxmenux-monitor</code> + <code>aud=api</code> claims; the signature is validated against the current secret on every request.",
|
||
"where": "Secret: <code>auth.json</code> → <code>jwt_secret</code>. JWT itself: only on the client."
|
||
},
|
||
{
|
||
"asset": "API token metadata",
|
||
"algorithm": "SHA-256 of the JWT stored alongside a <code>signed_with</code> fingerprint of the <code>jwt_secret</code> used to mint it — used to display the token in the UI and to detect tokens whose signing secret has been rotated.",
|
||
"where": "<code>auth.json</code> → <code>api_tokens[]</code>"
|
||
},
|
||
{
|
||
"asset": "2FA TOTP secret",
|
||
"algorithm": "Standard TOTP (RFC 6238), with a base32-encoded secret. The eight backup codes are single-use and stored as SHA-256 hashes.",
|
||
"where": "<code>auth.json</code> → <code>totp_secret</code> + <code>backup_codes[]</code>"
|
||
},
|
||
{
|
||
"asset": "Revocations",
|
||
"algorithm": "When 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).",
|
||
"where": "<code>auth.json</code> → <code>revoked_tokens[]</code>"
|
||
}
|
||
],
|
||
"authJsonTitle": "auth.json — what it contains and how it's protected",
|
||
"authJsonBody": "Everything ProxMenux Monitor needs to authenticate you lives in a single file: <code>/root/.config/proxmenux-monitor/auth.json</code>, mode <code>0600</code>, owner <code>root</code>. The file holds <em>hashes</em> (PBKDF2) and <em>signing material</em> (<code>jwt_secret</code>, <code>totp_secret</code>) — 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.",
|
||
"rotateTitle": "Rotating jwt_secret invalidates all existing JWTs",
|
||
"rotateBody": "If <code>auth.json</code> is regenerated (manual delete, reinstall, restore from a backup with a different secret) the <code>jwt_secret</code> 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 <strong>Invalid — regenerate</strong> badge so the operator knows to revoke and re-mint them; Home Assistant / scripts / any external client needs a fresh token after that.",
|
||
"recoverTitle": "Recovering a lost password",
|
||
"recoverIntro": "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 <code>auth.json</code>:",
|
||
"survivesTitle": "What survives the reset",
|
||
"survivesBody": "Only the interactive login is wiped. The <code>jwt_secret</code> and the registered <code>api_tokens</code> 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 <code>auth.json</code> manually and restart the service. The next launch generates a fresh secret and all old tokens become invalid.",
|
||
"physicalTitle": "Physical-access prerequisite",
|
||
"physicalBody": "This reset path needs <strong>root shell on the host</strong>. That is the trust anchor of the whole authentication scheme: anyone who can run <code>menu</code> 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."
|
||
},
|
||
"twofa": {
|
||
"heading": "Two-Factor Authentication (TOTP)",
|
||
"intro": "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 <strong>TOTP</strong> protocol (RFC 6238), so any authenticator app works.",
|
||
"pickTitle": "Pick an authenticator app",
|
||
"pickIntro": "If you already have a TOTP app, you can use it here. Common choices are listed below. Some are free and others require a subscription; supported platforms and backup methods also differ.",
|
||
"headerApp": "App",
|
||
"headerPlatforms": "Platforms",
|
||
"headerAppNotes": "Notes",
|
||
"apps": [
|
||
{
|
||
"name": "Google Authenticator",
|
||
"href": "https://safety.google/authentication/",
|
||
"platforms": "iOS, Android",
|
||
"notes": "The default for many users. Optional Google-account cloud backup."
|
||
},
|
||
{
|
||
"name": "Microsoft Authenticator",
|
||
"href": "https://www.microsoft.com/en-us/security/mobile-authenticator-app",
|
||
"platforms": "iOS, Android",
|
||
"notes": "Microsoft-account backup. Also handles MS push notifications if you use them at work."
|
||
},
|
||
{
|
||
"name": "Authy",
|
||
"href": "https://authy.com/",
|
||
"platforms": "iOS, Android",
|
||
"notes": "Encrypted sync across devices. The desktop app reached end of life on 19 March 2024 and is no longer supported."
|
||
},
|
||
{
|
||
"name": "Apple Passwords",
|
||
"href": "https://support.apple.com/guide/passwords/welcome/mac",
|
||
"platforms": "iOS, iPadOS, macOS, visionOS, Windows (via iCloud)",
|
||
"notes": "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."
|
||
},
|
||
{
|
||
"name": "Bitwarden",
|
||
"href": "https://bitwarden.com/",
|
||
"platforms": "iOS, Android, desktop, browser",
|
||
"notes": "Open source. TOTP can be stored alongside passwords in the vault. Keeping both in the same vault is convenient, but does not provide separation onto a second device."
|
||
},
|
||
{
|
||
"name": "1Password",
|
||
"href": "https://1password.com/",
|
||
"platforms": "iOS, Android, desktop, browser",
|
||
"notes": "Same idea as Bitwarden — TOTP integrated with the password vault. Subscription."
|
||
},
|
||
{
|
||
"name": "Aegis Authenticator",
|
||
"href": "https://getaegis.app/",
|
||
"platforms": "Android",
|
||
"notes": "Open source. Encrypted on-device backup file you control. No cloud, no account required."
|
||
},
|
||
{
|
||
"name": "Raivo OTP",
|
||
"href": "https://raivo-otp.com/",
|
||
"platforms": "iOS, macOS",
|
||
"notes": "Open source. Optional iCloud sync. The Apple-ecosystem counterpart to Aegis."
|
||
},
|
||
{
|
||
"name": "Ente Auth",
|
||
"href": "https://ente.io/auth/",
|
||
"platforms": "iOS, Android, desktop, web",
|
||
"notes": "Open source. End-to-end encrypted cloud sync across devices."
|
||
},
|
||
{
|
||
"name": "2FAS",
|
||
"href": "https://2fas.com/",
|
||
"platforms": "iOS, Android, browser extension",
|
||
"notes": "Open source. Optional encrypted cloud backup; browser extension can autofill codes."
|
||
},
|
||
{
|
||
"name": "FreeOTP+",
|
||
"href": "https://github.com/helloworld1/FreeOTPPlus",
|
||
"platforms": "Android",
|
||
"notes": "Open-source fork of FreeOTP for Android, with import/export features. No account required."
|
||
}
|
||
],
|
||
"backupTitle": "What \"backup\" really matters for",
|
||
"backupBody": "If you lose the authenticator device, use a saved backup code or restore the app from a backup you configured beforehand. Cloud sync and local exports differ by app; check that your chosen method actually restores the TOTP secrets. Root access to the host also allows recovery, as explained below. Keep a separate, secure backup.",
|
||
"setupTitle": "Step-by-step setup from the dashboard",
|
||
"setupImageAlt": "2FA setup screen with QR code and backup codes",
|
||
"setupImageCaption": "The 2FA setup dialog shows a QR code, a Base32 secret for manual entry and, after verification, eight one-time backup codes. Copy the codes before closing the dialog; they are not displayed again.",
|
||
"setupSteps": [
|
||
"<strong>Install the authenticator app on your phone</strong> (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.",
|
||
"<strong>Log into the dashboard</strong> with your username and password.",
|
||
"<strong>Open the Security tab</strong> in the dashboard sidebar, then click <strong>Enable 2FA</strong> and <strong>Start setup</strong>. The dialog shows a QR code and the manual key; the eight backup codes appear after the code is verified.",
|
||
"<strong>Add the entry to the authenticator app:</strong>",
|
||
"<strong>Confirm by typing the current 6-digit code</strong> 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.",
|
||
"<strong>Save the backup codes.</strong> After verification, store all eight safely in a password manager, encrypted note or secure printed copy. Each works once if your authenticator is unavailable.",
|
||
"<strong>Done.</strong> 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."
|
||
],
|
||
"setupStep4Sub": [
|
||
"<em>Easy path:</em> in the app, tap <em>Add account</em> → <em>Scan QR code</em>, point the camera at the QR on the screen. The app names the entry automatically (something like <code>ProxMenux Monitor (your-username)</code>) and starts showing a 6-digit code that refreshes every 30 seconds.",
|
||
"<em>Manual fallback</em> (when scanning isn't possible — e.g. setting up on the same phone you opened the dashboard with): tap <em>Add account</em> → <em>Enter setup key</em>. Type any name (e.g. <em>Proxmox Monitor</em>), paste the Base32 string from the dialog, leave <em>Type</em> as <em>Time-based</em>, save."
|
||
],
|
||
"testTitle": "Test before logging out",
|
||
"testBody": "Keep your current session open and test a new login in a private window <em>immediately</em> after setup. If the code is rejected, check the server and phone clocks before logging out of the working session. If access is already lost, use a backup code or edit <code>auth.json</code> from the host shell.",
|
||
"lostTitle": "Lost authenticator",
|
||
"lostIntro": "Three escape hatches, in order of how disruptive they are:",
|
||
"lostItems": [
|
||
"<strong>Use a backup code.</strong> Enter one of the eight saved codes in the TOTP field. It works once; unused codes remain valid. After logging in, open Security, choose <strong>Disable 2FA</strong> and then <strong>Enable 2FA</strong> to obtain eight new codes.",
|
||
"<strong>Restore the authenticator from cloud / backup.</strong> 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.",
|
||
"<strong>Disable 2FA from the host shell.</strong> When the previous options aren't available, edit <code>/root/.config/proxmenux-monitor/auth.json</code> on the Proxmox host (you need root SSH or console access), set <code>totp_enabled</code> to <code>false</code>, save, and restart the service:"
|
||
],
|
||
"lostShellOutro": "You can log in with username + password only, then re-enable 2FA from Settings.",
|
||
"disableTitle": "Disable 2FA",
|
||
"disableBody": "Open <strong>Security</strong> and choose <strong>Disable 2FA</strong>. <code>POST /api/auth/totp/disable</code> requires the current password and a valid TOTP or backup code. It removes the TOTP secret and backup codes. Remove the old entry from your authenticator app too.",
|
||
"rejectedTitle": "The 6-digit code is always rejected",
|
||
"rejectedIntro": "TOTP is time-based — server clock and phone clock must agree to within ~30 s. Two checks:",
|
||
"rejectedItems": [
|
||
"<strong>Phone:</strong> Settings → Date & Time → automatic / network sync ON.",
|
||
"<strong>Proxmox host:</strong> <code>timedatectl status</code> — \"System clock synchronized: yes\" should be visible. If not, <code>timedatectl set-ntp true</code> and wait a minute."
|
||
],
|
||
"rejectedOutro": "Once both clocks agree, the code is accepted within the next 30-second window."
|
||
},
|
||
"apiTokens": {
|
||
"heading": "API tokens (long-lived)",
|
||
"intro": "Browser sessions expire after 24 hours. For unattended integrations (Homepage widgets, Home Assistant sensors, Grafana scrapers, Uptime Kuma probes…) you generate a separate <strong>API token</strong> that lives 365 days. The token is a JWT signed with the same secret as the session token, but its <code>token_name</code> claim makes it easy to track and revoke individually.",
|
||
"imageAlt": "API tokens panel showing the token list with name, prefix, created date and expiry",
|
||
"imageCaption": "The API tokens list under Settings — name, prefix (last 4 chars are shown for identification), created and expiry dates, revoke action.",
|
||
"generateTitle": "Generate a token",
|
||
"generateIntro": "From the dashboard:",
|
||
"generateSteps": [
|
||
"Navigate to <strong>the Security tab → API Access Tokens</strong> section.",
|
||
"Type a descriptive name (<em>e.g. \"Home Assistant\"</em>).",
|
||
"Re-enter your password. If 2FA is on, also the current 6-digit code.",
|
||
"Click <strong>Generate Token</strong>. The token appears <strong>once</strong> — copy it immediately."
|
||
],
|
||
"generateCli": "From the command line:",
|
||
"useTitle": "Use a token",
|
||
"revokeTitle": "Revoke a token",
|
||
"revokeBody": "From the panel above: each row has a <strong>Revoke</strong> action that adds the token hash to <code>revoked_tokens</code> in <code>auth.json</code>. Revoked tokens fail validation immediately on the next request.",
|
||
"cheatTitle": "Token security cheat-sheet",
|
||
"cheatItems": [
|
||
"Store tokens in your integration's native secrets store — Homepage <code>secrets.yaml</code>, Home Assistant <code>!secret</code>, 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."
|
||
],
|
||
"outro": "Full storage best-practices and integration recipes live in <apiLink>API Reference → Token Management</apiLink> and <intLink>Integrations</intLink>."
|
||
},
|
||
"https": {
|
||
"heading": "HTTPS",
|
||
"intro": "Two paths to TLS:",
|
||
"items": [
|
||
"<strong>Reverse proxy (recommended).</strong> Terminate TLS on Nginx / Caddy / Traefik and forward HTTP on port 8008 to the Flask process. Snippets below.",
|
||
"<strong>Direct HTTPS in the AppImage.</strong> Configure a certificate with <code>POST /api/ssl/configure</code> (UI: <strong>Settings → SSL</strong>). The HTTPS path uses <code>gevent.pywsgi</code> with its standard handler; flask-sock handles WebSocket upgrades for WSS. Certificate paths are stored in the SSL configuration."
|
||
],
|
||
"calloutTitle": "Direct HTTPS limitations",
|
||
"calloutBody": "The Monitor can use a Proxmox-managed certificate or custom certificate files. It does not issue ACME certificates itself. For automatic issuance and renewal, use Proxmox ACME or a reverse proxy such as Caddy, Traefik or Nginx, and ensure renewed certificates are loaded."
|
||
},
|
||
"gateway": {
|
||
"heading": "Secure Gateway (Tailscale)",
|
||
"intro": "A reverse proxy is one way to provide remote access. <strong>Secure Gateway</strong> offers access without exposing TCP 8008: it deploys an Alpine LXC running <a>Tailscale</a> as a subnet router. Access to the host’s LAN IP depends on approved subnet routes and tailnet access rules.",
|
||
"calloutTitle": "Why this is convenient",
|
||
"calloutBody": "Use the LAN URL <code>http://<proxmox-lan-ip>:8008</code> through the approved subnet route. Tailscale subnet routers use SNAT by default, so the Monitor normally sees the gateway’s LAN address, not the client’s <code>100.x.y.z</code> address. Logs and Fail2Ban therefore identify the source address the host actually receives.",
|
||
"deployBody": "The deploy flow is one screen — pick the host LXC storage, paste a Tailscale auth-key (generated at <a>login.tailscale.com/admin/settings/keys</a>), choose which subnets to advertise, click Deploy. The LXC takes ~30 seconds to bootstrap and registers in the tailnet automatically.",
|
||
"outro": "Step-by-step deployment, subnet-routes configuration, Tailscale ACLs and Exit Node mode are documented separately in <link>Dashboard → Security → Secure Gateway</link> — that's where the deploy wizard lives in the dashboard UI. This page only covers the access pattern."
|
||
},
|
||
"proxy": {
|
||
"heading": "Reverse proxy snippets",
|
||
"intro": "Use a <strong>dedicated host name</strong> such as <code>monitor.example.com</code> forwarding to port 8008 on the Proxmox host. The examples follow this pattern. A sub-path such as <code>example.com/proxmenux-monitor/</code> needs additional frontend and proxy work; it is not supported by prefix stripping alone.",
|
||
"nginxTitle": "Nginx",
|
||
"caddyTitle": "Caddy",
|
||
"traefikTitle": "Traefik (labels — Docker / Kubernetes)",
|
||
"subPathTitle": "Advanced: sub-path mounts under an existing domain",
|
||
"subPathBody": "The current frontend has no configured sub-path base and builds API calls from root-relative <code>/api/*</code> URLs. Merely stripping a prefix such as <code>example.com/proxmenux-monitor/</code> does not handle those API calls or all frontend assets. Use a dedicated host name. A sub-path deployment requires coordinated frontend and proxy changes and separate testing."
|
||
},
|
||
"audit": {
|
||
"heading": "Audit log",
|
||
"intro": "Login failures and rate-limit warnings are written to <code>/var/log/proxmenux-auth.log</code> in a single-line format. Successful logins are not recorded by this logger:",
|
||
"outro": "Tail it the usual way: <code>tail -F /var/log/proxmenux-auth.log</code>. The file is rotated by <code>logrotate</code> if a config drop-in is added; the Monitor itself does not rotate it."
|
||
},
|
||
"fail2ban": {
|
||
"heading": "Optional: Fail2Ban jail",
|
||
"calloutTitle": "Fail2Ban is not bundled with the Monitor",
|
||
"calloutBody": "Fail2Ban is <strong>not</strong> installed by ProxMenux Monitor itself. Install it via <link>Security → Fail2Ban</link> 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.",
|
||
"intro": "When Fail2Ban is installed, the ProxMenux integration ships a <code>[proxmenux]</code> jail that:",
|
||
"items": [
|
||
"Reads <code>/var/log/proxmenux-auth.log</code>.",
|
||
"Matches the <code>authentication failure; rhost=<ip></code> pattern with a dedicated filter.",
|
||
"Bans the offending IP at the kernel firewall level by default.",
|
||
"The Flask <code>before_request</code> hook checks requests against the ban list, cached for 30 s. Behind a reverse proxy, client-IP blocking also requires PROXMENUX_TRUST_PROXY=1 and a controlled proxy that sets trustworthy forwarding headers."
|
||
],
|
||
"outro": "Configuration, ban time tuning and unban procedures are in <link>Security → Fail2Ban</link>."
|
||
},
|
||
"troubleshoot": {
|
||
"heading": "Troubleshooting",
|
||
"noScreenTitle": "The first-launch screen never appears",
|
||
"noScreenBody": "Either auth is already configured (<code>configured: true</code>) or somebody already chose Skip. To start fresh:",
|
||
"noScreenOutro": "This wipes the auth state — also any TOTP secrets and API tokens. Back up <code>auth.json</code> first if you have tokens you want to keep.",
|
||
"tokenTitle": "HTTP 401 on every request from a working API token",
|
||
"tokenBody": "Token expired (365 d limit) or got into the <code>revoked_tokens</code> list. Generate a new one in Settings and update the integration. To check:",
|
||
"tokenOutro": "Expired or revoked tokens return <code>'{'\"error\":\"Invalid or expired token\"'}'</code>.",
|
||
"no2faTitle": "Can't log in after enabling 2FA, no authenticator at hand",
|
||
"no2faBody": "Use a backup code in the TOTP field. If those are gone, edit <code>/root/.config/proxmenux-monitor/auth.json</code> from a host shell, set <code>totp_enabled</code> to <code>false</code>, restart the service.",
|
||
"wsTitle": "Reverse proxy works but the terminal tab disconnects every minute",
|
||
"wsBody": "WebSocket idle timeout in the proxy. Bump the read timeout (Nginx: <code>proxy_read_timeout 86400s</code>; Traefik: <code>idleTimeout</code> in the entry-point or middleware) and confirm <code>proxy_set_header Upgrade $http_upgrade</code> and <code>Connection \"upgrade\"</code> are present."
|
||
},
|
||
"whereNext": {
|
||
"heading": "Where to next",
|
||
"items": [
|
||
{
|
||
"label": "API Reference → Token Management",
|
||
"href": "/docs/monitor/api",
|
||
"tail": " — full lifecycle of API tokens (generate / list / revoke), security best-practices, secrets storage patterns."
|
||
},
|
||
{
|
||
"label": "Integrations",
|
||
"href": "/docs/monitor/integrations",
|
||
"tail": " — Homepage, Home Assistant, Grafana / Prometheus, Uptime Kuma, generic cURL."
|
||
},
|
||
{
|
||
"label": "Dashboard → Security → Secure Gateway",
|
||
"href": "/docs/monitor/dashboard/security",
|
||
"tail": " — deploy the Tailscale gateway LXC step by step (subnet routes, ACLs, Exit Node mode)."
|
||
},
|
||
{
|
||
"label": "Security → Fail2Ban",
|
||
"href": "/docs/security/fail2ban",
|
||
"tail": " — how to install and configure the optional jail."
|
||
},
|
||
{
|
||
"label": "Settings → ProxMenux Monitor",
|
||
"href": "/docs/settings/proxmenux-monitor",
|
||
"tail": " — start / stop the systemd service from the ProxMenux TUI."
|
||
}
|
||
]
|
||
}
|
||
}
|