PatchMon Application Documentation
- Welcome to PatchMon
- Installation
- Installing PatchMon Server on Docker
- Installing PatchMon Server on Ubuntu 24
- PatchMon Environment Variables Reference
- Installation of the PatchMon Agent
- First time setup admin page
- Installing PatchMon Server on K8S with Helm
- Nginx example configuration for PatchMon
- Integrations
- Integration API documentation (scoped credentials)
- Proxmox LXC Auto-Enrollment Guide
- Auto-enrolment api documentation
- Ansible Dynamic Library
- GetHomepage Integration Guide
- Setting up OIDC SSO Single Sign-on integration
- Release Notes Docs
- Agent Side Management
- Known issues & troubleshooting
- Software Architecture
- General Information
- Migrating from 1.4.2 to 1.5.0
Welcome to PatchMon
PatchMon is an open-source Linux patch management system that gives system administrators centralised visibility over patches and packages across their infrastructure.
It works with standard Linux package managers - apt, yum, and dnf - and requires no inbound ports on your monitored hosts.
Contributing to documentation
Documentation is an area where we need help :)
- You can signup with your email or use SSO using your github account
- After which we will enable your account to be a contributor so you can edit
- Please send us a message on Discord or email for your role to change to a contributor
How PatchMon Works
PatchMon uses a lightweight agent model with three simple steps:
- Deploy the Server - Self-host PatchMon using Docker or the native installer, or use our managed Cloud version.
- Install the Agent - Add a host in the dashboard and run the one-liner install command on your Linux server.
- Monitor - The agent sends system and package data outbound to PatchMon on a schedule. No inbound ports need to be opened on your servers.
Network requirements: Agents only need outbound access on port 443 (HTTPS). If your systems are behind firewalls that inspect SSL/DNS traffic or are air-gapped, adjust your rules accordingly.
Key Features
| Area | Details |
|---|---|
| Dashboard | Customisable per-user card layout with fleet-wide overview |
| Host Management | Host inventory, grouping, and OS detail tracking |
| Package Tracking | Package and Repo inventory, outdated package counts, and repository tracking per host |
| Agent System | Lightweight GO agents with outbound-only communication connected via Web Sockets |
| Users & Auth | Multi-user accounts with roles, permissions, and RBAC |
| OIDC SSO | Single Sign-On via external identity providers |
| API & Integrations | REST API for managing hosts and data, templates for getHomepage and others available |
| Proxmox Integration | Auto-enrollment for LXC containers from Proxmox hosts |
| BETA - Security Compliance | OpenSCAP CIS Benchmarks and Docker Bench for Security with scheduled and on-demand scans |
| Docker Inventory | Container discovery and tracking across your hosts |
| SSH Terminal | In-browser SSH terminal with AI assistance |
| Extensive Configuration | Configurable parameters using .env variables |
Quick Links
Architecture
End Users (Browser) ──HTTPS──▶ nginx (frontend + API proxy)
│
▼
Backend (Node.js / Express / Prisma)
│
▼
PostgreSQL
▲
Agents on your servers ──HTTPS──▶ Backend API (/api/v1)
(outbound only)
- Backend: Node.js, Express, Prisma ORM
- Frontend: Vite + React
- Database: PostgreSQL
- Reverse Proxy: nginx
- Service Management: systemd
Support
- Discord: patchmon.net/discord
- Email: support@patchmon.net
- GitHub Issues: Report a bug
License
PatchMon is licensed under AGPLv3.
Installation
This chapter is dedicated to the Installation of PatchMon Server and the Agent
Installing PatchMon Server on Docker
Overview
PatchMon runs as a containerised application made up of four services:
- Database - PostgreSQL 17
- Redis - Redis 7 (used for BullMQ job queues and caching)
- Backend - Node.js API server
- Frontend - React application served via NGINX
Container Images
- Backend: ghcr.io/patchmon/patchmon-backend
- Frontend: ghcr.io/patchmon/patchmon-frontend
Available Tags
| Tag | Description |
|---|---|
latest |
Latest stable release |
x.y.z |
Exact version pin (e.g. 1.2.3) |
x.y |
Latest patch in a minor series (e.g. 1.2) |
x |
Latest minor and patch in a major series (e.g. 1) |
edge |
Latest development build in main branch - may be unstable, for testing only |
Both backend and frontend images share the same version tags.
Quick Start
1. Download the files
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/docker/docker-compose.yml
curl -fsSL -o env.example https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/docker/env.example
2. Create your .env file
Copy the example file and generate the three required secrets:
cp env.example .env
# Generate and insert strong secrets
sed -i "s/^POSTGRES_PASSWORD=$/POSTGRES_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/^REDIS_PASSWORD=$/REDIS_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/^JWT_SECRET=$/JWT_SECRET=$(openssl rand -hex 64)/" .env
Then open .env and configure your server access settings:
# Set these to the URL you will use to access PatchMon.
# SERVER_PROTOCOL, SERVER_HOST and SERVER_PORT are used by agents to connect back to PatchMon.
# CORS_ORIGIN should match the full URL you access PatchMon from in your browser.
SERVER_PROTOCOL=http
SERVER_HOST=localhost
SERVER_PORT=3000
CORS_ORIGIN=http://localhost:3000
Tip: If you are deploying PatchMon behind a reverse proxy with a domain name, set
SERVER_PROTOCOL=https,SERVER_HOST=patchmon.example.com,SERVER_PORT=443, andCORS_ORIGIN=https://patchmon.example.com.
That's it. The docker-compose.yml uses env_file: .env to pass all your configuration straight into the containers. You do not need to edit the compose file itself.
For a full list of optional environment variables (rate limiting, logging, database pool tuning, session timeouts, etc.), see the Environment Variables page.
3. Start PatchMon
docker compose up -d
Once all containers are healthy, open your browser at http://localhost:3000 (or your configured URL) and complete the first-time setup to create your admin account.
Updating PatchMon
By default the compose file uses the latest tag. To update:
docker compose pull
docker compose up -d
This pulls the latest images, recreates containers, and keeps all your data intact.
Pinning to a specific version
If you prefer to pin versions, update the image tags in docker-compose.yml:
services:
backend:
image: ghcr.io/patchmon/patchmon-backend:1.4.0
frontend:
image: ghcr.io/patchmon/patchmon-frontend:1.4.0
Then run:
docker compose pull
docker compose up -d
Check the releases page for version-specific changes and migration notes.
Volumes
The compose file creates the following Docker volumes:
| Volume | Purpose |
|---|---|
postgres_data |
PostgreSQL data directory |
redis_data |
Redis data directory |
agent_files |
PatchMon agent scripts |
branding_assets |
Custom branding files (logos, favicons) - optional, new in 1.4.0 |
You can bind any of these to a host path instead of a Docker volume by editing the compose file.
Note: The backend container runs as user and group ID 1000. If you rebind the
agent_filesorbranding_assetsvolumes to a host path, make sure that user/group has write permissions.
Docker Swarm Deployment
This section covers deploying PatchMon to a Docker Swarm cluster. For standard single-host deployments, use the production guide above.
Network Configuration
The default compose file uses an internal bridge network (patchmon-internal) for service-to-service communication. All services connect to this network and discover each other by service name.
If you are using an external reverse proxy network (e.g. Traefik):
- Keep all PatchMon services on
patchmon-internalfor internal communication - Optionally add the frontend service to the reverse proxy network
- Make sure service name resolution works within the same network
Example: Traefik integration
services:
frontend:
image: ghcr.io/patchmon/patchmon-frontend:latest
networks:
- patchmon-internal
deploy:
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.routers.patchmon.rule=Host(`patchmon.example.com`)"
The frontend reaches the backend via patchmon-internal using the hostname backend, while Traefik routes external traffic to the frontend.
Troubleshooting: host not found in upstream "backend"
This usually means the frontend and backend are on different networks, or services haven't fully started. Check:
- All services are on the same internal network
- Service health status with
docker psordocker service ps - Network connectivity with
docker exec <container> ping backend
Development Setup
This section is for developers who want to contribute to PatchMon or run it locally in development mode.
Getting started
git clone https://github.com/PatchMon/PatchMon.git
cd PatchMon
docker compose -f docker/docker-compose.dev.yml up --watch --build
The development compose file:
- Builds images locally from source
- Enables hot reload via Docker Compose watch
- Exposes all service ports for debugging
- Mounts source code directly into containers
Development ports
| Service | Port | URL |
|---|---|---|
| Frontend | 3000 | http://localhost:3000 |
| Backend API | 3001 | http://localhost:3001 |
| PostgreSQL | 5432 | localhost:5432 |
| Redis | 6379 | localhost:6379 |
Common commands
# Start with hot reload (attached)
docker compose -f docker/docker-compose.dev.yml up --watch
# Start detached
docker compose -f docker/docker-compose.dev.yml up -d
# Rebuild a specific service
docker compose -f docker/docker-compose.dev.yml up -d --build backend
# View logs
docker compose -f docker/docker-compose.dev.yml logs -f
How hot reload works
- Frontend/backend source changes are synced automatically in watch mode
- package.json changes trigger an automatic service rebuild
- Prisma schema changes cause the backend to restart automatically
Building images locally
# Development images
docker build -f docker/backend.Dockerfile --target development -t patchmon-backend:dev .
docker build -f docker/frontend.Dockerfile --target development -t patchmon-frontend:dev .
# Production images
docker build -f docker/backend.Dockerfile -t patchmon-backend:latest .
docker build -f docker/frontend.Dockerfile -t patchmon-frontend:latest .
Installing PatchMon Server on Ubuntu 24
Overview
The PatchMon setup script automates the full installation on Ubuntu/Debian servers. It installs all dependencies, configures services, generates credentials, and starts PatchMon - ready to use in minutes.
It supports both fresh installations and updating existing instances.
Requirements
| Requirement | Minimum |
|---|---|
| OS | Ubuntu 20.04+ / Debian 11+ |
| CPU | 2 vCPU |
| RAM | 2 GB |
| Disk | 15 GB |
| Access | Root (sudo) |
| Network | Internet access (to pull packages and clone the repo) |
Fresh Installation
1. Prepare the server
apt-get update -y && apt-get upgrade -y
apt install curl jq bc -y
2. Run the setup script
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh \
&& chmod +x setup.sh \
&& sudo bash setup.sh
3. Follow the interactive prompts
The script will ask you four things:
| Prompt | Description | Default |
|---|---|---|
| Domain/IP | The public DNS or local IP users will access PatchMon from | patchmon.internal |
| SSL/HTTPS | Enable Let's Encrypt SSL. Use y for public servers, n for internal networks |
n |
| Only asked if SSL is enabled - used for Let's Encrypt certificate notifications | - | |
| Release / Branch | Lists the latest 3 release tags plus main. Pick the latest release unless you need a specific version |
Latest tag |
After confirming your choices, the script runs fully unattended.
What the Script Does
The script performs these steps automatically:
- Checks timezone - confirms (or lets you change) the server timezone
- Installs prerequisites -
curl,jq,git,wget,netcat-openbsd,sudo - Installs Node.js 20.x - via NodeSource
- Installs PostgreSQL - creates an isolated database and user for this instance
- Installs Redis - configures ACL-based authentication with a dedicated Redis user and database
- Installs Nginx - sets up a reverse proxy with security headers
- Installs Certbot (if SSL enabled) - obtains and configures a Let's Encrypt certificate
- Creates a dedicated system user - PatchMon runs as a non-login, locked-down user
- Clones the repository to
/opt/<your-domain>/ - Installs npm dependencies in an isolated environment
- Creates
.envfiles - generates secrets and writesbackend/.envandfrontend/.env - Runs database migrations - with self-healing for failed migrations
- Creates a systemd service - with
NoNewPrivileges,PrivateTmp, andProtectSystem=strict - Configures Nginx - reverse proxy with HTTP/2, WebSocket support, and security headers
- Populates server settings in the database (server URL, protocol, port)
- Writes
deployment-info.txt- all credentials and commands in one file
After Installation
- Visit
http(s)://<your-domain>in your browser - Complete the first-time admin setup (create your admin account)
- All credentials and useful commands are saved to:
/opt/<your-domain>/deployment-info.txt
Directory Structure
After installation, PatchMon lives at /opt/<your-domain>/:
/opt/<your-domain>/
backend/
.env # Backend environment variables
src/
prisma/
frontend/
.env # Frontend environment variables (baked at build)
dist/ # Built frontend (served by Nginx)
deployment-info.txt # Credentials, ports, and diagnostic commands
patchmon-install.log
Environment Variables
The setup script generates a backend/.env with sensible defaults. You can customise it after installation.
File location: /opt/<your-domain>/backend/.env
Variables set by the script
| Variable | What the script sets |
|---|---|
DATABASE_URL |
Full connection string with generated password |
JWT_SECRET |
Auto-generated 50-character secret |
CORS_ORIGIN |
<protocol>://<your-domain> |
PORT |
Random port between 3001-3999 |
REDIS_HOST |
localhost |
REDIS_PORT |
6379 |
REDIS_USER |
Instance-specific Redis ACL user |
REDIS_PASSWORD |
Auto-generated password |
REDIS_DB |
Auto-detected available Redis database |
Adding optional variables
To enable OIDC, adjust rate limits, configure TFA, or change other settings, add the relevant variables to backend/.env and restart the service.
For example, to enable OIDC SSO:
# Edit the .env file
sudo nano /opt/<your-domain>/backend/.env
Add at the bottom:
# OIDC / SSO
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://auth.example.com
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
Then restart:
sudo systemctl restart <your-domain>
Full list of optional variables
All optional environment variables are documented in the Docker env.example file and on the Environment Variables page. The same variables work for both Docker and native installations. Key categories include:
- Authentication -
JWT_EXPIRES_IN,JWT_REFRESH_EXPIRES_IN,SESSION_INACTIVITY_TIMEOUT_MINUTES - Password policy -
PASSWORD_MIN_LENGTH,PASSWORD_REQUIRE_UPPERCASE, etc. - Account lockout -
MAX_LOGIN_ATTEMPTS,LOCKOUT_DURATION_MINUTES - Two-factor authentication -
MAX_TFA_ATTEMPTS,TFA_REMEMBER_ME_EXPIRES_IN, etc. - OIDC / SSO -
OIDC_ENABLED,OIDC_ISSUER_URL,OIDC_CLIENT_ID, etc. - Rate limiting -
RATE_LIMIT_WINDOW_MS,RATE_LIMIT_MAX,AUTH_RATE_LIMIT_*,AGENT_RATE_LIMIT_* - Database pool -
DB_CONNECTION_LIMIT,DB_POOL_TIMEOUT,DB_IDLE_TIMEOUT, etc. - Logging -
LOG_LEVEL,ENABLE_LOGGING - Network -
ENABLE_HSTS,TRUST_PROXY,CORS_ORIGINS - Encryption -
AI_ENCRYPTION_KEY,SESSION_SECRET - Timezone -
TZ - Body limits -
JSON_BODY_LIMIT,AGENT_UPDATE_BODY_LIMIT
After any
.envchange, restart the service:sudo systemctl restart <your-domain>
Updating an Existing Installation
To update PatchMon to the latest version, re-run the setup script with --update:
sudo bash setup.sh --update
The update process:
- Detects all existing PatchMon installations under
/opt/ - Lets you select which instance to update
- Backs up the current code and database before making changes
- Pulls the latest code from the selected branch/tag
- Installs updated dependencies and rebuilds the frontend
- Runs any new database migrations (with self-healing)
- Adds any missing environment variables to
backend/.env(preserves your existing values) - Updates the Nginx configuration with latest security improvements
- Restarts the service
If the update fails, the script prints rollback instructions with the exact commands to restore from the backup.
Managing the Service
Replace <your-domain> with the domain/IP you used during installation (e.g. patchmon.internal).
Service commands
# Check status
systemctl status <your-domain>
# Restart
sudo systemctl restart <your-domain>
# Stop
sudo systemctl stop <your-domain>
# View logs (live)
journalctl -u <your-domain> -f
# View recent logs
journalctl -u <your-domain> --since "1 hour ago"
Other useful commands
# Test Nginx config
nginx -t && sudo systemctl reload nginx
# Check database connection
sudo -u <db-user> psql -d <db-name> -c "SELECT 1;"
# Check which port PatchMon is listening on
netstat -tlnp | grep <backend-port>
# View deployment info (credentials, ports, etc.)
cat /opt/<your-domain>/deployment-info.txt
Troubleshooting
| Issue | Solution |
|---|---|
| Script fails with permission error | Run with sudo bash setup.sh |
| Service won't start | Check logs: journalctl -u <your-domain> -n 50 |
| Redis authentication error | Verify REDIS_USER and REDIS_PASSWORD in backend/.env match Redis ACL. Run redis-cli ACL LIST to check |
| Database connection refused | Check PostgreSQL is running: systemctl status postgresql |
| SSL certificate issues | Run certbot certificates to check status. Renew with certbot renew |
| Nginx 502 Bad Gateway | Backend may not be running. Check systemctl status <your-domain> and the backend port |
| Migration failures | Check status: cd /opt/<your-domain>/backend && npx prisma migrate status |
| Port already in use | The script picks a random port (3001-3999). Edit PORT in backend/.env and update the Nginx config |
For more help, see the Troubleshooting page or check the installation log:
cat /opt/<your-domain>/patchmon-install.log
Please note: This script was built to automate the deployment of PatchMon however our preferred method of installation is via Docker. This method is hard to support due to various parameters and changes within the OS such as versions of Nginx causing issues on the installer.
Anyway, do enjoy and I understand if you're like me ... want to see the files in plain sight that is being served as the app ;)
PatchMon Environment Variables Reference
This document provides a comprehensive reference for all environment variables available in PatchMon. These variables can be configured in your backend/.env file (bare metal installations) or in the .env file alongside docker-compose.yml (Docker deployments using env_file).
Database Configuration
PostgreSQL database connection settings.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
DATABASE_URL |
PostgreSQL connection string | - | Yes | postgresql://user:pass@localhost:5432/patchmon_db |
PM_DB_CONN_MAX_ATTEMPTS |
Maximum database connection attempts during startup | 30 |
No | 30 |
PM_DB_CONN_WAIT_INTERVAL |
Wait interval between connection attempts (seconds) | 2 |
No | 2 |
Usage Notes
- The
DATABASE_URLmust be a valid PostgreSQL connection string - Connection retry logic helps handle database startup delays in containerized environments
- Format:
postgresql://[user]:[password]@[host]:[port]/[database] - In Docker deployments,
DATABASE_URLis constructed automatically in the compose file - you do not set it in.env
Database Connection Pool (Prisma)
Connection pooling configuration for optimal database performance and resource management.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
DB_CONNECTION_LIMIT |
Maximum number of database connections per instance | 30 |
No | 30 |
DB_POOL_TIMEOUT |
Seconds to wait for an available connection before timeout | 20 |
No | 20 |
DB_CONNECT_TIMEOUT |
Seconds to wait for initial database connection | 10 |
No | 10 |
DB_IDLE_TIMEOUT |
Seconds before closing idle connections | 300 |
No | 300 |
DB_MAX_LIFETIME |
Maximum lifetime of a connection in seconds | 1800 |
No | 1800 |
Sizing Guidelines
Small Deployment (1-10 hosts):
DB_CONNECTION_LIMIT=15
DB_POOL_TIMEOUT=20
Medium Deployment (10-50 hosts):
DB_CONNECTION_LIMIT=30 # Default
DB_POOL_TIMEOUT=20
Large Deployment (50+ hosts):
DB_CONNECTION_LIMIT=50
DB_POOL_TIMEOUT=30
Connection Pool Calculation
Use this formula to estimate your needs:
DB_CONNECTION_LIMIT = (expected_hosts * 2) + (concurrent_users * 2) + 5
Example: 20 hosts + 3 concurrent users:
(20 * 2) + (3 * 2) + 5 = 51 connections
Important Notes
- Each backend instance maintains its own connection pool
- Running multiple backend instances requires considering total connections to PostgreSQL
- PostgreSQL default
max_connectionsis 100 (ensure your pool size doesn't exceed this) - Connections are reused efficiently - you don't need one connection per host
- Increase pool size if experiencing timeout errors during high load
Detecting Connection Pool Issues
When connection pool limits are hit, you'll see clear error messages in your backend console:
Typical Pool Timeout Error:
Host creation error: Error: Timed out fetching a new connection from the connection pool.
DATABASE CONNECTION POOL EXHAUSTED!
Current limit: DB_CONNECTION_LIMIT=30
Pool timeout: DB_POOL_TIMEOUT=20s
Suggestion: Increase DB_CONNECTION_LIMIT in your .env file
If you see these errors frequently, increase DB_CONNECTION_LIMIT by 10-20 and monitor your system.
Monitoring Connection Pool Usage
You can monitor your PostgreSQL connections to determine optimal pool size:
Check Current Connections:
# Connect to PostgreSQL
psql -U patchmon_user -d patchmon_db
# Run this query
SELECT count(*) as current_connections,
(SELECT setting::int FROM pg_settings WHERE name='max_connections') as max_connections
FROM pg_stat_activity
WHERE datname = 'patchmon_db';
Recommended Actions:
- If
current_connectionsfrequently approachesDB_CONNECTION_LIMIT, increase the pool size - Monitor during peak usage (when multiple users are active, agents checking in)
- Leave 20-30% headroom for burst traffic
Database Transaction Timeouts
Control how long database transactions can run before being terminated.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
DB_TRANSACTION_MAX_WAIT |
Maximum time (ms) to wait for a transaction to start | 10000 |
No | 10000 |
DB_TRANSACTION_TIMEOUT |
Maximum time (ms) for a standard transaction to complete | 30000 |
No | 30000 |
DB_TRANSACTION_LONG_TIMEOUT |
Maximum time (ms) for long-running transactions (e.g. bulk operations) | 60000 |
No | 60000 |
Usage Notes
- These prevent runaway queries from holding database locks indefinitely
- Increase
DB_TRANSACTION_LONG_TIMEOUTif bulk import or migration operations are timing out - All values are in milliseconds
Authentication & Security
JWT token configuration and security settings.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
JWT_SECRET |
Secret key for signing JWT tokens | - | Yes | your-secure-random-secret-key |
JWT_EXPIRES_IN |
Access token expiration time | 1h |
No | 1h, 30m, 2h |
JWT_REFRESH_EXPIRES_IN |
Refresh token expiration time | 7d |
No | 7d, 3d, 14d |
Generating Secure Secrets
# Linux/macOS
openssl rand -hex 64
Time Format
Supports the following formats:
s: secondsm: minutesh: hoursd: days
Examples: 30s, 15m, 2h, 7d
Recommended Settings
Development:
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
Production:
JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=3d
High Security:
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=1d
Password Policy
Enforce password complexity requirements for local user accounts.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
PASSWORD_MIN_LENGTH |
Minimum password length | 8 |
No | 8, 12, 16 |
PASSWORD_REQUIRE_UPPERCASE |
Require at least one uppercase letter | true |
No | true, false |
PASSWORD_REQUIRE_LOWERCASE |
Require at least one lowercase letter | true |
No | true, false |
PASSWORD_REQUIRE_NUMBER |
Require at least one number | true |
No | true, false |
PASSWORD_REQUIRE_SPECIAL |
Require at least one special character | true |
No | true, false |
PASSWORD_RATE_LIMIT_WINDOW_MS |
Rate limit window for password changes (ms) | 900000 |
No | 900000 (15 min) |
PASSWORD_RATE_LIMIT_MAX |
Maximum password change attempts per window | 5 |
No | 5 |
Recommended Settings
Standard (default):
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true
High Security:
PASSWORD_MIN_LENGTH=12
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true
PASSWORD_RATE_LIMIT_MAX=3
Usage Notes
- These rules apply to local accounts only - OIDC users authenticate against their identity provider
- Password changes and new account creation both enforce these rules
PASSWORD_RATE_LIMIT_*prevents brute-force password change attempts
Account Lockout
Protect against brute-force login attacks by temporarily locking accounts after repeated failures.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
MAX_LOGIN_ATTEMPTS |
Failed login attempts before account lockout | 5 |
No | 5, 3, 10 |
LOCKOUT_DURATION_MINUTES |
Minutes the account stays locked after exceeding attempts | 15 |
No | 15, 30, 60 |
Usage Notes
- Lockout is per-account, not per-IP
- The failed attempts counter has a 15-minute rolling window - if no further failed attempts occur within that window, the counter resets on its own
- A successful login clears the failed attempts counter (before lockout is triggered)
- Once locked out, the account stays locked for the full
LOCKOUT_DURATION_MINUTES- there is no way to bypass this except waiting - Setting
MAX_LOGIN_ATTEMPTStoo low may lock out legitimate users who mistype passwords
Recommended Settings
Standard:
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION_MINUTES=15
High Security:
MAX_LOGIN_ATTEMPTS=3
LOCKOUT_DURATION_MINUTES=30
Session Management
Control user session behavior and security.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
SESSION_INACTIVITY_TIMEOUT_MINUTES |
Minutes of inactivity before automatic logout | 30 |
No | 30 |
Usage Notes
- Sessions are tracked in the database with activity timestamps
- Each authenticated request updates the session activity
- Expired sessions are automatically invalidated
- Users must log in again after timeout period
- Lower values provide better security but may impact user experience
Recommended Settings
- High Security Environment:
15minutes - Standard Security:
30minutes (default) - User-Friendly:
60minutes
Two-Factor Authentication (TFA)
Settings for two-factor authentication when users have it enabled.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
MAX_TFA_ATTEMPTS |
Failed TFA code attempts before lockout | 5 |
No | 5, 3 |
TFA_LOCKOUT_DURATION_MINUTES |
Minutes locked out after exceeding TFA attempts | 30 |
No | 30, 60 |
TFA_REMEMBER_ME_EXPIRES_IN |
"Remember this device" token expiration | 30d |
No | 30d, 7d, 90d |
TFA_MAX_REMEMBER_SESSIONS |
Maximum remembered devices per user | 5 |
No | 5 |
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD |
Failed attempts before flagging suspicious activity | 3 |
No | 3 |
Usage Notes
- These variables only apply when users have TFA enabled on their account
- "Remember this device" allows users to skip TFA on trusted devices
MAX_TFA_ATTEMPTSandTFA_LOCKOUT_DURATION_MINUTESprevent brute-force attacks on TOTP codes- Suspicious activity detection can trigger additional security measures
- Remembered sessions can be revoked by users or admins
Recommended Settings
Standard:
MAX_TFA_ATTEMPTS=5
TFA_LOCKOUT_DURATION_MINUTES=30
TFA_REMEMBER_ME_EXPIRES_IN=30d
TFA_MAX_REMEMBER_SESSIONS=5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
High Security:
MAX_TFA_ATTEMPTS=3
TFA_LOCKOUT_DURATION_MINUTES=60
TFA_REMEMBER_ME_EXPIRES_IN=7d
TFA_MAX_REMEMBER_SESSIONS=3
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=2
OIDC / SSO
OpenID Connect configuration for Single Sign-On. Set OIDC_ENABLED=true and fill in your identity provider details to enable SSO.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
OIDC_ENABLED |
Enable OIDC authentication | false |
No | true, false |
OIDC_ISSUER_URL |
Identity provider issuer URL | - | If OIDC enabled | https://auth.example.com |
OIDC_CLIENT_ID |
OAuth client ID | - | If OIDC enabled | patchmon |
OIDC_CLIENT_SECRET |
OAuth client secret | - | If OIDC enabled | your-client-secret |
OIDC_REDIRECT_URI |
Callback URL after authentication | - | If OIDC enabled | https://patchmon.example.com/api/v1/auth/oidc/callback |
OIDC_SCOPES |
OAuth scopes to request | openid email profile groups |
No | openid email profile groups |
OIDC_AUTO_CREATE_USERS |
Automatically create PatchMon accounts for new OIDC users | true |
No | true, false |
OIDC_DEFAULT_ROLE |
Default role for auto-created OIDC users | user |
No | user, admin, viewer |
OIDC_DISABLE_LOCAL_AUTH |
Disable local username/password login when OIDC is enabled | false |
No | true, false |
OIDC_BUTTON_TEXT |
Login button text shown on the login page | Login with SSO |
No | Login with SSO, Sign in with Authentik |
Group-to-Role Mapping
Map OIDC groups from your identity provider to PatchMon roles. This keeps role assignments in sync with your IdP.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
OIDC_ADMIN_GROUP |
OIDC group name that maps to admin role | - | No | PatchMon Admins |
OIDC_USER_GROUP |
OIDC group name that maps to user role | - | No | PatchMon Users |
OIDC_SYNC_ROLES |
Sync roles from OIDC groups on each login | true |
No | true, false |
Example: Authentik
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example.com/application/o/patchmon/
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Authentik
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true
Example: Keycloak
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://keycloak.example.com/realms/your-realm
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Keycloak
Usage Notes
OIDC_REDIRECT_URImust be registered as an allowed redirect URI in your identity provider- When
OIDC_DISABLE_LOCAL_AUTH=true, users can only log in via OIDC - useful for enforcing SSO across the organisation - When
OIDC_SYNC_ROLES=true, the user's role is updated on every login based on their OIDC group membership - If a user is in both
OIDC_ADMIN_GROUPandOIDC_USER_GROUP, the admin role takes precedence - The
groupsscope must be supported by your identity provider and included inOIDC_SCOPESfor group mapping to work
Server & Network Configuration
Server protocol, host, and CORS settings.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
PORT |
Backend API server port | 3001 |
No | 3001 |
NODE_ENV |
Node.js environment mode | production |
No | production, development |
SERVER_PROTOCOL |
Server protocol | http |
No | http, https |
SERVER_HOST |
Server hostname/domain | localhost |
No | patchmon.example.com |
SERVER_PORT |
Server port | 3000 |
No | 3000, 443 |
CORS_ORIGIN |
Allowed CORS origin URL | http://localhost:3000 |
No | https://patchmon.example.com |
CORS_ORIGINS |
Multiple allowed CORS origins (comma-separated) | - | No | https://a.example.com,https://b.example.com |
ENABLE_HSTS |
Enable HTTP Strict Transport Security | true |
No | true, false |
TRUST_PROXY |
Trust proxy headers (when behind reverse proxy) | true |
No | true, false |
Usage Notes
SERVER_PROTOCOL,SERVER_HOST, andSERVER_PORTare used to generate agent installation scriptsCORS_ORIGINmust match the URL you use to access PatchMon in your browserCORS_ORIGINS(plural, comma-separated) overridesCORS_ORIGINwhen set - only needed if PatchMon is accessed from multiple domains- Set
TRUST_PROXYtotruewhen behind nginx, Apache, or other reverse proxies ENABLE_HSTSshould betruein production with HTTPS
Example Configurations
Local Development:
SERVER_PROTOCOL=http
SERVER_HOST=localhost
SERVER_PORT=3000
CORS_ORIGIN=http://localhost:3000
ENABLE_HSTS=false
TRUST_PROXY=false
Production with HTTPS:
SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGIN=https://patchmon.example.com
ENABLE_HSTS=true
TRUST_PROXY=true
Multiple Domains:
SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGINS=https://patchmon.example.com,https://patchmon-alt.example.com
ENABLE_HSTS=true
TRUST_PROXY=true
Rate Limiting
Protect your API from abuse with configurable rate limits.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
RATE_LIMIT_WINDOW_MS |
General rate limit window (milliseconds) | 900000 |
No | 900000 (15 min) |
RATE_LIMIT_MAX |
Maximum requests per window (general) | 5000 |
No | 5000 |
AUTH_RATE_LIMIT_WINDOW_MS |
Authentication endpoints rate limit window (ms) | 600000 |
No | 600000 (10 min) |
AUTH_RATE_LIMIT_MAX |
Maximum auth requests per window | 500 |
No | 500 |
AGENT_RATE_LIMIT_WINDOW_MS |
Agent API rate limit window (ms) | 60000 |
No | 60000 (1 min) |
AGENT_RATE_LIMIT_MAX |
Maximum agent requests per window | 1000 |
No | 1000 |
Understanding Rate Limits
Rate limits are applied per IP address and endpoint category:
- General API: Dashboard, hosts, packages, user management
- Authentication: Login, logout, token refresh
- Agent API: Agent check-ins, updates, package reports
Calculating Windows
The window is a sliding time frame. Examples:
900000ms = 15 minutes600000ms = 10 minutes60000ms = 1 minute
Recommended Settings
Default (Balanced):
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX=5000 # ~5.5 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=500 # ~0.8 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=1000 # ~16 requests/second
Strict (High Security):
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX=2000 # ~2.2 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=100 # ~0.16 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=500 # ~8 requests/second
Relaxed (Development/Testing):
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX=10000 # ~11 requests/second
AUTH_RATE_LIMIT_WINDOW_MS=600000 # 10 minutes
AUTH_RATE_LIMIT_MAX=1000 # ~1.6 requests/second
AGENT_RATE_LIMIT_WINDOW_MS=60000 # 1 minute
AGENT_RATE_LIMIT_MAX=2000 # ~33 requests/second
Redis Configuration
Redis is used for BullMQ job queues and caching.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
REDIS_HOST |
Redis server hostname | localhost |
No | localhost, redis, 10.0.0.5 |
REDIS_PORT |
Redis server port | 6379 |
No | 6379 |
REDIS_USER |
Redis username (Redis 6+) | - | No | default |
REDIS_PASSWORD |
Redis authentication password | - | Recommended | your-redis-password |
REDIS_DB |
Redis database number | 0 |
No | 0, 1, 2 |
Usage Notes
- Redis authentication is highly recommended for security
- Redis 6.0+ supports ACL with usernames; earlier versions use password-only auth
- If no password is set, Redis will be accessible without authentication (not recommended)
- Database number allows multiple applications to use the same Redis instance
Docker Deployment
In Docker, set REDIS_PASSWORD in your .env file. The compose file automatically passes it to both the Redis container (via its startup command) and the backend service (via env_file).
Bare Metal Deployment
The setup script configures Redis ACL with a dedicated user and password per instance. The credentials are written to backend/.env automatically.
Generating Secure Passwords
openssl rand -hex 32
Logging
Control application logging behavior and verbosity.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
LOG_LEVEL |
Logging level | info |
No | debug, info, warn, error |
ENABLE_LOGGING |
Enable/disable application logging | true |
No | true, false |
PM_LOG_TO_CONSOLE |
Output logs to the console | false |
No | true, false |
PM_LOG_REQUESTS_IN_DEV |
Log HTTP requests in development mode | false |
No | true, false |
PRISMA_LOG_QUERIES |
Log all Prisma database queries | false |
No | true, false |
Log Levels
Ordered from most to least verbose:
debug: All logs including database queries, internal operationsinfo: General information, startup messages, normal operationswarn: Warning messages, deprecated features, non-critical issueserror: Error messages only, critical issues
Recommended Settings
Development:
LOG_LEVEL=debug
ENABLE_LOGGING=true
PM_LOG_TO_CONSOLE=true
PM_LOG_REQUESTS_IN_DEV=true
PRISMA_LOG_QUERIES=true
Production:
LOG_LEVEL=info
ENABLE_LOGGING=true
PM_LOG_TO_CONSOLE=false
PRISMA_LOG_QUERIES=false
Production (Quiet):
LOG_LEVEL=warn
ENABLE_LOGGING=true
PRISMA_LOG_QUERIES=false
Timezone Configuration
Control timezone handling for timestamps and logs across the application.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
TZ |
Timezone for timestamps and logs | UTC |
No | UTC, America/New_York, Europe/London |
Usage Notes
- The
TZenvironment variable controls timezone handling across all components:- Backend (Node.js): Timestamps in API responses, database records, logs
- Agent (Go): Agent logs, integration data timestamps
- If
TZis not set, the application defaults to UTC - Database timestamps are always stored in UTC for consistency
- Display timestamps can be converted to the configured timezone
Common Timezone Values
# UTC (recommended for servers)
TZ=UTC
# UK
TZ=Europe/London
# US
TZ=America/New_York # Eastern
TZ=America/Chicago # Central
TZ=America/Los_Angeles # Pacific
# Europe
TZ=Europe/Paris
TZ=Europe/Berlin
# Asia
TZ=Asia/Tokyo
TZ=Asia/Shanghai
Body Size Limits
Control the maximum size of request bodies accepted by the API.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
JSON_BODY_LIMIT |
Maximum JSON request body size | 5mb |
No | 5mb, 10mb, 1mb |
AGENT_UPDATE_BODY_LIMIT |
Maximum body size for agent update payloads | 2mb |
No | 2mb, 5mb |
Usage Notes
JSON_BODY_LIMITapplies to all standard API endpoints (dashboard actions, user management, etc.)AGENT_UPDATE_BODY_LIMITapplies specifically to agent check-in and package report payloads- Increase these if agents are managing a very large number of packages and the payload exceeds the limit
- Keep these as low as practical to limit memory usage and reduce the impact of oversized requests
Encryption
Controls encryption of sensitive data stored in the database (e.g. AI provider API keys, bootstrap tokens).
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
AI_ENCRYPTION_KEY |
Encryption key for sensitive data at rest (64 hex characters) | - | No | Output of openssl rand -hex 32 |
SESSION_SECRET |
Fallback key used if AI_ENCRYPTION_KEY is not set |
- | No | Output of openssl rand -hex 32 |
How It Works
The backend uses this priority chain to determine the encryption key:
AI_ENCRYPTION_KEY- used directly if set (64 hex chars = 32 bytes, or any string which gets SHA-256 hashed)SESSION_SECRET- ifAI_ENCRYPTION_KEYis not set, SHA-256 hashed to derive the keyDATABASE_URL- if neither above is set, derives a key from the database URL (logs a security warning)- Ephemeral - last resort, generates a random key (data encrypted with this key will be unreadable after a restart)
What Gets Encrypted
- AI API keys - API keys for AI providers (e.g. OpenAI) are AES-256-GCM encrypted before being stored in the database
- Bootstrap tokens - Agent auto-enrollment API keys are encrypted before temporary storage in Redis
Usage Notes
- For most deployments, you do not need to set either variable - the key is derived from
DATABASE_URLwhich is stable - Set
AI_ENCRYPTION_KEYif you need encryption stability across database URL changes or multi-replica deployments - These do not affect user password storage (passwords are bcrypt hashed, not encrypted)
User Management
Default settings for new users.
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
DEFAULT_USER_ROLE |
Default role assigned to new users | user |
No | user, admin, viewer |
Available Roles
admin: Full system access, can manage users and settingsuser: Standard access, can manage hosts and packagesviewer: Read-only access, cannot make changes
Usage Notes
- Only applies to newly created users
- Existing users are not affected by changes to this variable
- First user created through setup is always an admin
- Can be changed per-user through the user management interface
Frontend Configuration
Frontend-specific environment variables (used during build and runtime).
| Variable | Description | Default | Required | Example |
|---|---|---|---|---|
VITE_API_URL |
Backend API base URL | /api/v1 |
No | http://localhost:3001/api/v1 |
VITE_APP_NAME |
Application name displayed in UI | PatchMon |
No | PatchMon |
VITE_APP_VERSION |
Application version displayed in UI | (from package.json) | No | 1.4.0 |
BACKEND_HOST |
Backend hostname (Docker only) | backend |
No | backend, localhost |
BACKEND_PORT |
Backend port (Docker only) | 3001 |
No | 3001 |
VITE_ENABLE_LOGGING |
Enable frontend debug logging | false |
No | true, false |
Usage Notes
- Frontend variables are prefixed with
VITE_for the Vite build system VITE_*variables are embedded at build time - they cannot be changed at runtimeVITE_API_URLcan be relative (/api/v1) or absoluteBACKEND_HOSTandBACKEND_PORTare used by the Docker frontend container's Nginx proxy config
Complete Example Configuration
Bare Metal (Production)
# Database
DATABASE_URL="postgresql://patchmon_user:secure_db_password@localhost:5432/patchmon_db"
PM_DB_CONN_MAX_ATTEMPTS=30
PM_DB_CONN_WAIT_INTERVAL=2
# Database Connection Pool
DB_CONNECTION_LIMIT=30
DB_POOL_TIMEOUT=20
DB_CONNECT_TIMEOUT=10
DB_IDLE_TIMEOUT=300
DB_MAX_LIFETIME=1800
# JWT
JWT_SECRET="generated-secure-secret-from-openssl"
JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=3d
# Server
PORT=3001
NODE_ENV=production
SERVER_PROTOCOL=https
SERVER_HOST=patchmon.example.com
SERVER_PORT=443
CORS_ORIGIN=https://patchmon.example.com
ENABLE_HSTS=true
TRUST_PROXY=true
# Session
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
# User
DEFAULT_USER_ROLE=user
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=5000
AUTH_RATE_LIMIT_WINDOW_MS=600000
AUTH_RATE_LIMIT_MAX=500
AGENT_RATE_LIMIT_WINDOW_MS=60000
AGENT_RATE_LIMIT_MAX=1000
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=secure_redis_password
REDIS_DB=0
# Logging
LOG_LEVEL=info
ENABLE_LOGGING=true
# Timezone
TZ=UTC
# TFA
TFA_REMEMBER_ME_EXPIRES_IN=30d
TFA_MAX_REMEMBER_SESSIONS=5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
Docker (Production)
For Docker deployments, see docker/env.example for a complete template. Copy it to .env, fill in the required values, and run docker compose up -d. The compose file reads all variables via env_file: .env.
Troubleshooting
Common Issues
"timeout of 10000ms exceeded" when adding hosts:
- Increase
DB_CONNECTION_LIMIT(try 30 or higher) - Increase
DB_POOL_TIMEOUT(try 20 or 30) - Check backend logs for "DATABASE CONNECTION POOL EXHAUSTED" messages
Database connection failures on startup:
- Increase
PM_DB_CONN_MAX_ATTEMPTS - Increase
PM_DB_CONN_WAIT_INTERVAL - Verify
DATABASE_URLis correct
"Invalid or expired session" errors:
- Check
JWT_SECREThasn't changed between restarts - Verify
SESSION_INACTIVITY_TIMEOUT_MINUTESisn't too low - Ensure
JWT_EXPIRES_INis reasonable
Rate limit errors (429 Too Many Requests):
- Increase
RATE_LIMIT_MAXvalues - Increase window duration (
*_WINDOW_MSvariables)
CORS errors:
- Verify
CORS_ORIGINmatches your frontend URL exactly (protocol + domain + port) - For multiple domains, use
CORS_ORIGINS(plural, comma-separated)
OIDC login fails:
- Verify
OIDC_REDIRECT_URIis registered in your identity provider - Check
OIDC_ISSUER_URLis reachable from the PatchMon server - Ensure
OIDC_CLIENT_SECRETmatches the value in your IdP
Encrypted data unreadable after restart:
- Set
AI_ENCRYPTION_KEYto a stable value so the key persists across restarts - Re-enter AI provider API keys if the encryption key has changed
Security Best Practices
-
Always set strong secrets:
- Use
openssl rand -hex 64forJWT_SECRET - Use
openssl rand -hex 32for database and Redis passwords
- Use
-
Enable HTTPS in production:
- Set
SERVER_PROTOCOL=https - Enable
ENABLE_HSTS=true - Use proper SSL certificates
- Set
-
Configure appropriate rate limits:
- Adjust based on expected traffic
- Lower limits for public-facing deployments
-
Use session timeouts:
- Don't set
SESSION_INACTIVITY_TIMEOUT_MINUTEStoo high - Balance security with user experience
- Don't set
-
Secure Redis:
- Always set
REDIS_PASSWORD - Use Redis ACLs in Redis 6+ for additional security
- Don't expose Redis port publicly
- Always set
-
Enable account lockout:
- Keep
MAX_LOGIN_ATTEMPTSandLOCKOUT_DURATION_MINUTESat defaults or stricter
- Keep
-
Enforce password policy:
- Keep all
PASSWORD_REQUIRE_*options enabled - Consider increasing
PASSWORD_MIN_LENGTHto 12+
- Keep all
Version Information
- Last Updated: February 2026
- Applicable to PatchMon: v1.4.0+
Installation of the PatchMon Agent
Introduction
Note : This is pre version 1.3.0 , new one needs to be made
The agent installer script authenticates with PatchMon server and proceeds to install the patchmon-agent.sh.
Adding a host:
Go to the Hosts page, press Add host:
You will be greeted by this screen where you enter in a friendly name for the server:
Groups will show empty unless you have added groups already.
Here is how to add groups
As soon as you press "Create Host" it will redirect you to the credentials and deployment script page:
Now we have two ways of installation however we should first take care of a few things:
The first is the checkbox "Force Install (bypass broken packages)"
At times in Linux, certain packages are broken and throw dependency issues when installing software which break the installation script. If you find this is happening then ideally you need to fix your server before you install more software, hoever that's not always possible so by pressing this flag it will bypass package manager issues.
Secondly, at times you may have a self-signed certificate in which this curl command needs an extra flag to go through. This would apply to both the Installation command as well as the agent communication within. Here is how to enable the -k flag installation-wide in settings.
Installing the agent in the Automated way
The one-line command includes the api-id and api-key which is utilized to download the PatchMon agent as well as to populate the credentials.txt file.
SSH into your server, ensure you are root and lets copy and paste this one-line command and see the output:
🕐 Verifying system datetime and timezone...
📅 Current System Date/Time:
• Date/Time: Sat Oct 4 07:16:00 PM BST 2025
• Timezone: Europe/London
⚠️ Non-interactive installation detected
Please verify the date/time shown above is correct.
If the date/time is incorrect, it may cause issues with:
• Logging timestamps
• Scheduled updates
• Data synchronization
✅ Continuing with installation...
✅ ✅ Date/time verification completed (assumed correct)
ℹ️ 🚀 Starting PatchMon Agent Installation...
ℹ️ 📋 Server: https://demo09.eu-west-02.patchmon.cloud
ℹ️ 🔑 API ID: patchmon_90dd7fe...
ℹ️ 🆔 Machine ID: a9295dcd387f979b...
🔧 Installation Diagnostics:
• URL: https://demo09.eu-west-02.patchmon.cloud
• CURL FLAGS: -s
• API ID: patchmon_90dd7fe...
• API Key: 49ff8a6f6b67abf5...
ℹ️ 📦 Installing required dependencies...
ℹ️ Detected apt-get (Debian/Ubuntu)
ℹ️ Updating package lists...
Hit:1 https://cli.github.com/packages stable InRelease
Hit:2 https://linux.teamviewer.com/deb stable InRelease
Hit:3 https://download.docker.com/linux/ubuntu jammy InRelease
Hit:4 https://ppa.launchpadcontent.net/damentz/liquorix/ubuntu jammy InRelease
Hit:5 https://deb.nodesource.com/node_20.x nodistro InRelease
Hit:6 https://packages.microsoft.com/ubuntu/20.04/prod focal InRelease
Hit:7 https://packages.microsoft.com/repos/code stable InRelease
Hit:8 http://apt.pop-os.org/proprietary jammy InRelease
Hit:9 http://apt.pop-os.org/release jammy InRelease
Hit:10 https://apt.postgresql.org/pub/repos/apt jammy-pgdg InRelease
Hit:12 https://ngrok-agent.s3.amazonaws.com buster InRelease
Hit:13 http://apt.pop-os.org/ubuntu jammy InRelease
Hit:14 http://apt.pop-os.org/ubuntu jammy-security InRelease
Hit:15 http://apt.pop-os.org/ubuntu jammy-updates InRelease
Hit:11 https://packagecloud.io/slacktechnologies/slack/debian jessie InRelease
Hit:16 http://apt.pop-os.org/ubuntu jammy-backports InRelease
Reading package lists... Done
N: Skipping acquire of configured file 'main/binary-i386/Packages' as repository 'https://apt.postgresql.org/pub/repos/apt jammy-pgdg InRelease' doesn't support architecture 'i386'
ℹ️ Installing jq, curl, and bc...
✅ All required packages are already installed
✅ Dependencies installation completed
ℹ️ 📁 Setting up configuration directory...
ℹ️ 📁 Creating new configuration directory...
ℹ️ 🔐 Creating API credentials file...
ℹ️ 📥 Downloading PatchMon agent script...
ℹ️ 📋 Agent version: 1.2.7
ℹ️ 🔍 Checking if machine is already enrolled...
✅ ✅ Machine not yet enrolled - proceeding with installation
ℹ️ 🧪 Testing API credentials and connectivity...
✅ API credentials are valid
✅ ✅ TEST: API credentials are valid and server is reachable
ℹ️ 📊 Sending initial package data to server...
ℹ️ Verifying system datetime and timezone...
ℹ️ Collecting system information...
ℹ️ Sending update to PatchMon server...
✅ Update sent successfully (190 packages processed)
ℹ️ Checking crontab configuration...
ℹ️ Updating crontab with current policy...
ℹ️ Setting update interval to 60 minutes
✅ Crontab updated successfully (duplicates removed)
✅ Crontab updated successfully
✅ ✅ UPDATE: Initial package data sent successfully
ℹ️ ✅ Automated updates configured by agent
✅ 🎉 PatchMon Agent installation completed successfully!
📋 Installation Summary:
• Configuration directory: /etc/patchmon
• Agent installed: /usr/local/bin/patchmon-agent.sh
• Dependencies installed: jq, curl, bc
• Automated updates configured via crontab
• API credentials configured and tested
• Update schedule managed by agent
🔧 Management Commands:
• Test connection: /usr/local/bin/patchmon-agent.sh test
• Manual update: /usr/local/bin/patchmon-agent.sh update
• Check status: /usr/local/bin/patchmon-agent.sh diagnostics
✅ ✅ Your system is now being monitored by PatchMon!
Lets go through what this script has done:
- Asked you if the Time is correct, this is for informational purposes and it should be correct if it isn't. It causes issues with logs etc. Set that if it isn't right
- The script wants to install
jq bc curlFor the agent to work properly.
- It will install the credentials file in the location :
/etc/patchmon/credentials
It's important to note that this credentials file includes the PATCHMON_URL="https://yourinstance.domiain", Ensure this is correct if you have issues connecting to the agent after changing urls or ports
The Agent script location is in the following location:
/usr/local/bin/patchmon-agent.sh
Once the installation is done, Click off the page in PatchMon and refresh the hosts page.
You will see the information of your host now showing along with all the other information:
First time setup admin page
First time admin setup
Upon first time setup you will see this page:
Enter the details and your password (min 8 characters).
Try not using the username "admin" as it's a common one, but something unique to you.
After pressing Create account you will be redirected to the dashboard:
First thing is please setup MFA but going to your profile on the bottom left, you will be greeted with the Profile modification page, please press the Multi-Factor authentication tab and set that up:
If you get errors upon trying to login or create the admin account fort he first time, then ensure you have correctly setup the CORS url setting in your .env file located (/opt/<your instance>/backend/.env) or the docker environment file.
Installing PatchMon Server on K8S with Helm
PatchMon Helm Chart Documentation
Helm chart for deploying PatchMon on Kubernetes.
- Chart repository: github.com/RuTHlessBEat200/PatchMon-helm
- Application repository: github.com/PatchMon/PatchMon
Overview
PatchMon runs as a containerised application made up of four services:
- Database -- PostgreSQL 18 (StatefulSet with persistent storage)
- Redis -- Redis 8 (used for BullMQ job queues and caching, StatefulSet with persistent storage)
- Backend -- Node.js API server (Deployment with optional HPA)
- Frontend -- React application served via NGINX (Deployment with optional HPA)
The chart deploys all four components into a single namespace and wires them together automatically using init containers, internal ClusterIP services, and a shared ConfigMap.
Container Images
| Component | Image | Default Tag |
|---|---|---|
| Backend | ghcr.io/patchmon/patchmon-backend | 1.4.2 |
| Frontend | ghcr.io/patchmon/patchmon-frontend | 1.4.2 |
| Database | docker.io/postgres | 18-alpine |
| Redis | docker.io/redis | 8-alpine |
Available Tags (Backend and Frontend)
Both backend and frontend images share the same version tags.
| Tag | Description |
|---|---|
latest |
Latest stable release |
x.y.z |
Exact version pin (e.g. 1.4.2) |
x.y |
Latest patch in a minor series (e.g. 1.4) |
x |
Latest minor and patch in a major series (e.g. 1) |
edge |
Latest development build from the main branch -- may be unstable, for testing only |
Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- A PersistentVolume provisioner in the cluster (for database, Redis, and backend agent-file storage)
- (Optional) An Ingress controller (e.g. NGINX Ingress) for external access
- (Optional) cert-manager for automatic TLS certificate management
- (Optional) Metrics Server for HPA functionality
Quick Start
The quickest way to get PatchMon running is to use the provided
values-quick-start.yaml file.
It contains all required secrets inline and sensible defaults so you can install
with a single command.
Warning:
values-quick-start.yamlships with placeholder secrets and is intended for evaluation and testing only. Never use it in production without replacing all secret values.
1. Install the chart
wget https://github.com/RuTHlessBEat200/PatchMon-helm/blob/main/values-quick-start.yaml
helm install patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--create-namespace \
--values values-quick-start.yaml
2. Wait for pods to become ready
kubectl get pods -n patchmon -w
3. Access PatchMon
If ingress is enabled, open the host you configured (e.g. https://patchmon-dev.example.com).
Without ingress, use port-forwarding:
kubectl port-forward -n patchmon svc/patchmon-dev-frontend 3000:3000
Production Deployment
For production use, refer to the provided
values-prod.yaml file as a starting point.
It demonstrates how to:
- Use an external secret (e.g. managed by KSOPS, Sealed Secrets, or External Secrets Operator) instead of inline passwords
- Configure HTTPS with cert-manager
- Set the correct server protocol, host, and port for agent communication
1. Create your secrets
The chart does not auto-generate secrets. You must supply them yourself.
Required secrets:
| Key | Description |
|---|---|
postgres-password |
PostgreSQL password |
redis-password |
Redis password |
jwt-secret |
JWT signing secret for the backend |
ai-encryption-key |
Encryption key for AI provider credentials |
oidc-client-secret |
OIDC client secret (only if OIDC is enabled) |
You can either:
- Set passwords directly in your values file (
database.auth.password,redis.auth.password,backend.jwtSecret,backend.aiEncryptionKey), or - Create a Kubernetes Secret separately and reference it with
existingSecret/existingSecretPasswordKeyfields.
Example -- creating a secret manually:
kubectl create namespace patchmon
kubectl create secret generic patchmon-secrets \
--namespace patchmon \
--from-literal=postgres-password="$(openssl rand -hex 32)" \
--from-literal=redis-password="$(openssl rand -hex 32)" \
--from-literal=jwt-secret="$(openssl rand -hex 64)" \
--from-literal=ai-encryption-key="$(openssl rand -hex 32)"
Secret management tools for production:
- KSOPS -- encrypt secrets in Git using Mozilla SOPS
- Sealed Secrets -- encrypt secrets that only the cluster can decrypt
- External Secrets Operator -- sync secrets from external stores (Vault, AWS Secrets Manager, etc.)
- Vault -- enterprise-grade secret management
2. Create your values file
Start from values-prod.yaml and adjust to your environment:
global:
storageClass: "your-storage-class"
fullnameOverride: "patchmon-prod"
backend:
env:
serverProtocol: https
serverHost: patchmon.example.com
serverPort: "443"
corsOrigin: https://patchmon.example.com
existingSecret: "patchmon-secrets"
existingSecretJwtKey: "jwt-secret"
existingSecretAiEncryptionKey: "ai-encryption-key"
database:
auth:
existingSecret: patchmon-secrets
existingSecretPasswordKey: postgres-password
redis:
auth:
existingSecret: patchmon-secrets
existingSecretPasswordKey: redis-password
secret:
create: false # Disable chart-managed secret since we use an external one
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "0"
hosts:
- host: patchmon.example.com
paths:
- path: /
pathType: Prefix
service:
name: frontend
port: 3000
- path: /api
pathType: Prefix
service:
name: backend
port: 3001
tls:
- secretName: patchmon-tls
hosts:
- patchmon.example.com
3. Install
helm install patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--create-namespace \
--values values-prod.yaml
Configuration Reference
Global Settings
| Parameter | Description | Default |
|---|---|---|
global.imageRegistry |
Override the image registry for all components | "" |
global.imageTag |
Override the image tag for backend and frontend (if set, takes priority over individual tags) | "" |
global.imagePullSecrets |
Image pull secrets applied to all pods | [] |
global.storageClass |
Default storage class for all PVCs | "" |
nameOverride |
Override the chart name used in resource names | "" |
fullnameOverride |
Override the full resource name prefix | "" |
commonLabels |
Labels added to all resources | {} |
commonAnnotations |
Annotations added to all resources | {} |
Database (PostgreSQL)
| Parameter | Description | Default |
|---|---|---|
database.enabled |
Deploy the PostgreSQL StatefulSet | true |
database.image.registry |
Image registry | docker.io |
database.image.repository |
Image repository | postgres |
database.image.tag |
Image tag | 18-alpine |
database.image.pullPolicy |
Image pull policy | IfNotPresent |
database.auth.database |
Database name | patchmon_db |
database.auth.username |
Database user | patchmon_user |
database.auth.password |
Database password (required if existingSecret is not set) |
"" |
database.auth.existingSecret |
Name of an existing secret containing the password | "" |
database.auth.existingSecretPasswordKey |
Key inside the existing secret | postgres-password |
database.replicaCount |
Number of replicas | 1 |
database.updateStrategy.type |
StatefulSet update strategy | RollingUpdate |
database.persistence.enabled |
Enable persistent storage | true |
database.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
database.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
database.persistence.size |
PVC size | 5Gi |
database.resources.requests.cpu |
CPU request | 100m |
database.resources.requests.memory |
Memory request | 128Mi |
database.resources.limits.cpu |
CPU limit | 1000m |
database.resources.limits.memory |
Memory limit | 1Gi |
database.service.type |
Service type | ClusterIP |
database.service.port |
Service port | 5432 |
database.nodeSelector |
Node selector | {} |
database.tolerations |
Tolerations | [] |
database.affinity |
Affinity rules | {} |
Redis
| Parameter | Description | Default |
|---|---|---|
redis.enabled |
Deploy the Redis StatefulSet | true |
redis.image.registry |
Image registry | docker.io |
redis.image.repository |
Image repository | redis |
redis.image.tag |
Image tag | 8-alpine |
redis.image.pullPolicy |
Image pull policy | IfNotPresent |
redis.auth.password |
Redis password (required if existingSecret is not set) |
"" |
redis.auth.existingSecret |
Name of an existing secret containing the password | "" |
redis.auth.existingSecretPasswordKey |
Key inside the existing secret | redis-password |
redis.replicaCount |
Number of replicas | 1 |
redis.updateStrategy.type |
StatefulSet update strategy | RollingUpdate |
redis.persistence.enabled |
Enable persistent storage | true |
redis.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
redis.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
redis.persistence.size |
PVC size | 5Gi |
redis.resources.requests.cpu |
CPU request | 50m |
redis.resources.requests.memory |
Memory request | 10Mi |
redis.resources.limits.cpu |
CPU limit | 500m |
redis.resources.limits.memory |
Memory limit | 512Mi |
redis.service.type |
Service type | ClusterIP |
redis.service.port |
Service port | 6379 |
redis.nodeSelector |
Node selector | {} |
redis.tolerations |
Tolerations | [] |
redis.affinity |
Affinity rules | {} |
Backend
| Parameter | Description | Default |
|---|---|---|
backend.enabled |
Deploy the backend | true |
backend.image.registry |
Image registry | ghcr.io |
backend.image.repository |
Image repository | patchmon/patchmon-backend |
backend.image.tag |
Image tag (overridden by global.imageTag if set) |
1.4.2 |
backend.image.pullPolicy |
Image pull policy | Always |
backend.replicaCount |
Number of replicas (>1 requires RWX storage for agent files) | 1 |
backend.updateStrategy.type |
Deployment update strategy | Recreate |
backend.jwtSecret |
JWT signing secret (required if existingSecret is not set) |
"" |
backend.aiEncryptionKey |
AI encryption key (required if existingSecret is not set) |
"" |
backend.existingSecret |
Name of an existing secret for JWT and AI encryption key | "" |
backend.existingSecretJwtKey |
Key for JWT secret inside the existing secret | jwt-secret |
backend.existingSecretAiEncryptionKey |
Key for AI encryption key inside the existing secret | ai-encryption-key |
backend.persistence.enabled |
Enable persistent storage for agent files | true |
backend.persistence.storageClass |
Storage class (falls back to global.storageClass) |
"" |
backend.persistence.accessModes |
PVC access modes | ["ReadWriteOnce"] |
backend.persistence.size |
PVC size | 5Gi |
backend.resources.requests.cpu |
CPU request | 10m |
backend.resources.requests.memory |
Memory request | 256Mi |
backend.resources.limits.cpu |
CPU limit | 2000m |
backend.resources.limits.memory |
Memory limit | 2Gi |
backend.service.type |
Service type | ClusterIP |
backend.service.port |
Service port | 3001 |
backend.autoscaling.enabled |
Enable HPA (requires RWX storage if >1 replica) | false |
backend.autoscaling.minReplicas |
Minimum replicas | 1 |
backend.autoscaling.maxReplicas |
Maximum replicas | 10 |
backend.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilisation | 80 |
backend.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilisation | 80 |
backend.initContainers.waitForDatabase.enabled |
Wait for database before starting | true |
backend.initContainers.waitForRedis.enabled |
Wait for Redis before starting | true |
backend.initContainers.fixPermissions.enabled |
Run a privileged init container to fix file permissions | false |
backend.nodeSelector |
Node selector | {} |
backend.tolerations |
Tolerations | [] |
backend.affinity |
Affinity rules | {} |
Backend Environment Variables
| Parameter | Description | Default |
|---|---|---|
backend.env.enableLogging |
Enable application logging | true |
backend.env.logLevel |
Log level (trace, debug, info, warn, error) |
info |
backend.env.logToConsole |
Log to stdout | true |
backend.env.serverProtocol |
Protocol used by agents to reach the backend (http or https) |
http |
backend.env.serverHost |
Hostname used by agents to reach the backend | patchmon.example.com |
backend.env.serverPort |
Port used by agents (80 or 443) |
80 |
backend.env.corsOrigin |
CORS allowed origin (should match the URL users access in a browser) | http://patchmon.example.com |
backend.env.dbConnectionLimit |
Database connection pool limit | 30 |
backend.env.dbPoolTimeout |
Pool timeout in seconds | 20 |
backend.env.dbConnectTimeout |
Connection timeout in seconds | 10 |
backend.env.dbIdleTimeout |
Idle connection timeout in seconds | 300 |
backend.env.dbMaxLifetime |
Max connection lifetime in seconds | 1800 |
backend.env.rateLimitWindowMs |
General rate limit window (ms) | 900000 |
backend.env.rateLimitMax |
General rate limit max requests | 5000 |
backend.env.authRateLimitWindowMs |
Auth rate limit window (ms) | 600000 |
backend.env.authRateLimitMax |
Auth rate limit max requests | 500 |
backend.env.agentRateLimitWindowMs |
Agent rate limit window (ms) | 60000 |
backend.env.agentRateLimitMax |
Agent rate limit max requests | 1000 |
backend.env.redisDb |
Redis database index | 0 |
backend.env.trustProxy |
Trust proxy headers (set to true or a number behind a reverse proxy) |
false |
backend.env.enableHsts |
Enable HSTS header | false |
backend.env.defaultUserRole |
Default role for new users | user |
backend.env.autoCreateRolePermissions |
Auto-create role permissions | false |
OIDC / SSO Configuration
| Parameter | Description | Default |
|---|---|---|
backend.oidc.enabled |
Enable OIDC authentication | false |
backend.oidc.issuerUrl |
OIDC issuer URL | "" |
backend.oidc.clientId |
OIDC client ID | "" |
backend.oidc.clientSecret |
OIDC client secret (required if existingSecret not set) |
"" |
backend.oidc.existingSecret |
Existing secret containing the OIDC client secret | "" |
backend.oidc.existingSecretClientSecretKey |
Key inside the existing secret | oidc-client-secret |
backend.oidc.scopes |
OIDC scopes | openid profile email |
backend.oidc.buttonText |
Login button text | Login with SSO |
backend.oidc.autoCreateUsers |
Auto-create users on first OIDC login | false |
backend.oidc.defaultRole |
Default role for OIDC-created users | user |
backend.oidc.syncRoles |
Sync roles from OIDC group claims | false |
backend.oidc.disableLocalAuth |
Disable local username/password authentication | false |
backend.oidc.sessionTtl |
OIDC session TTL in seconds | 86400 |
backend.oidc.groups.superadmin |
OIDC group mapped to the superadmin role | "" |
backend.oidc.groups.admin |
OIDC group mapped to the admin role | "" |
backend.oidc.groups.hostManager |
OIDC group mapped to the hostManager role | "" |
backend.oidc.groups.user |
OIDC group mapped to the user role | "" |
backend.oidc.groups.readonly |
OIDC group mapped to the readonly role | "" |
Frontend
| Parameter | Description | Default |
|---|---|---|
frontend.enabled |
Deploy the frontend | true |
frontend.image.registry |
Image registry | ghcr.io |
frontend.image.repository |
Image repository | patchmon/patchmon-frontend |
frontend.image.tag |
Image tag (overridden by global.imageTag if set) |
1.4.2 |
frontend.image.pullPolicy |
Image pull policy | IfNotPresent |
frontend.replicaCount |
Number of replicas | 1 |
frontend.updateStrategy.type |
Deployment update strategy | Recreate |
frontend.resources.requests.cpu |
CPU request | 10m |
frontend.resources.requests.memory |
Memory request | 50Mi |
frontend.resources.limits.cpu |
CPU limit | 1000m |
frontend.resources.limits.memory |
Memory limit | 512Mi |
frontend.service.type |
Service type | ClusterIP |
frontend.service.port |
Service port | 3000 |
frontend.autoscaling.enabled |
Enable HPA | false |
frontend.autoscaling.minReplicas |
Minimum replicas | 1 |
frontend.autoscaling.maxReplicas |
Maximum replicas | 10 |
frontend.autoscaling.targetCPUUtilizationPercentage |
Target CPU utilisation | 80 |
frontend.autoscaling.targetMemoryUtilizationPercentage |
Target memory utilisation | 80 |
frontend.initContainers.waitForBackend.enabled |
Wait for backend before starting | true |
frontend.nodeSelector |
Node selector | {} |
frontend.tolerations |
Tolerations | [] |
frontend.affinity |
Affinity rules | {} |
Ingress
| Parameter | Description | Default |
|---|---|---|
ingress.enabled |
Enable ingress resource | true |
ingress.className |
Ingress class name | "" |
ingress.annotations |
Ingress annotations | {} |
ingress.hosts |
List of ingress host rules | see values.yaml |
ingress.tls |
TLS configuration | [] (disabled) |
Other
| Parameter | Description | Default |
|---|---|---|
serviceAccount.create |
Create a ServiceAccount | false |
serviceAccount.annotations |
ServiceAccount annotations | {} |
serviceAccount.name |
ServiceAccount name | "" |
configMap.create |
Create the application ConfigMap | true |
configMap.annotations |
ConfigMap annotations | {} |
secret.create |
Create the chart-managed Secret (disable when using an external secret) | true |
secret.annotations |
Secret annotations | {} |
Persistent Volumes
The chart creates the following PersistentVolumeClaims:
| PVC | Component | Purpose | Default Size |
|---|---|---|---|
postgres-data |
Database | PostgreSQL data directory | 5Gi |
redis-data |
Redis | Redis data directory | 5Gi |
agent-files |
Backend | PatchMon agent scripts and branding assets | 5Gi |
All PVCs respect the global.storageClass setting unless overridden at the component level.
Note: The backend container runs as UID/GID 1000. If you use
hostPathvolumes or a storage provider that does not respectfsGroup, you may need to enablebackend.initContainers.fixPermissions.enabled(requires privileged init containers).
Updating PatchMon
Using global.imageTag
The simplest way to update both backend and frontend at once is to set global.imageTag:
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
-n patchmon \
-f values-prod.yaml \
--set global.imageTag=1.5.0
When global.imageTag is set it overrides both backend.image.tag and frontend.image.tag.
Pinning individual tags
You can also set each tag independently:
backend:
image:
tag: "1.4.2"
frontend:
image:
tag: "1.4.2"
Upgrading the chart version
# Upgrade with new values
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--values values-prod.yaml
# Upgrade and wait for rollout
helm upgrade patchmon oci://ghcr.io/ruthlessbeat200/charts/patchmon \
--namespace patchmon \
--values values-prod.yaml \
--wait --timeout 10m
Check the releases page for version-specific changes and migration notes.
Uninstalling
# Uninstall the release
helm uninstall patchmon -n patchmon
# Clean up PVCs (optional -- this deletes all data)
kubectl delete pvc -n patchmon -l app.kubernetes.io/instance=patchmon
Advanced Configuration
Custom Image Registry
Override the registry for all images (useful for air-gapped environments or private mirrors):
global:
imageRegistry: "registry.example.com"
This changes every image pull to use the specified registry:
registry.example.com/postgres:18-alpineregistry.example.com/redis:8-alpineregistry.example.com/patchmon/patchmon-backend:1.4.2registry.example.com/patchmon/patchmon-frontend:1.4.2registry.example.com/busybox:latest(init containers)
Without global.imageRegistry, components use their default registries (docker.io for database/Redis, ghcr.io for backend/frontend).
Multi-Tenant Deployment
Deploy multiple isolated instances in separate namespaces using fullnameOverride:
fullnameOverride: "patchmon-tenant-a"
backend:
env:
serverHost: tenant-a.patchmon.example.com
corsOrigin: https://tenant-a.patchmon.example.com
ingress:
hosts:
- host: tenant-a.patchmon.example.com
paths:
- path: /
pathType: Prefix
service:
name: frontend
port: 3000
- path: /api
pathType: Prefix
service:
name: backend
port: 3001
Horizontal Pod Autoscaling
backend:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
persistence:
accessModes:
- ReadWriteMany # RWX required when running multiple replicas
frontend:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
Note: Scaling the backend beyond one replica requires a storage class that supports
ReadWriteMany(RWX) access mode, because all replicas need write access to agent files.
Using an External Database
Disable the built-in database and point the backend at an external PostgreSQL instance:
database:
enabled: false
backend:
env:
# Configure the external database connection via environment variables
# or adjust your external DB settings accordingly
OIDC / SSO Integration
backend:
oidc:
enabled: true
issuerUrl: "https://auth.example.com/realms/master"
clientId: "patchmon"
clientSecret: "your-client-secret"
scopes: "openid profile email groups"
buttonText: "Login with SSO"
autoCreateUsers: true
syncRoles: true
groups:
superadmin: "patchmon-admins"
admin: ""
hostManager: ""
user: ""
readonly: ""
Troubleshooting
Check pod status
kubectl get pods -n patchmon
kubectl describe pod <pod-name> -n patchmon
kubectl logs <pod-name> -n patchmon
Check init container logs
kubectl logs <pod-name> -n patchmon -c wait-for-database
kubectl logs <pod-name> -n patchmon -c wait-for-redis
kubectl logs <pod-name> -n patchmon -c wait-for-backend
Check service connectivity
# Test database connection
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- nc -zv patchmon-prod-database 5432
# Test Redis connection
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- nc -zv patchmon-prod-redis 6379
# Check backend health
kubectl exec -n patchmon -it deployment/patchmon-prod-backend -- wget -qO- http://localhost:3001/health
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
Pods stuck in Init state |
Database or Redis not yet running | Check StatefulSet events: kubectl describe sts -n patchmon |
PVC stuck in Pending |
No matching StorageClass or no available PV | Verify storage class exists: kubectl get sc |
ImagePullBackOff |
Registry credentials missing or incorrect image reference | Check imagePullSecrets and image path |
| Ingress returns 404 / 502 | Ingress controller not installed or misconfigured path rules | Verify controller pods and ingress resource: kubectl describe ingress -n patchmon |
secret ... not found |
Required secret was not created before install | Create the secret or set secret.create: true with inline passwords |
Development
Lint the chart
helm lint .
Render templates locally
# Render with default values
helm template patchmon . --values values-quick-start.yaml
# Render with production values
helm template patchmon . --values values-prod.yaml
# Debug template rendering
helm template patchmon . --values values-quick-start.yaml --debug
Dry-run installation
helm install patchmon . \
--namespace patchmon \
--dry-run --debug \
--values values-quick-start.yaml
Support
- GitHub Issues: github.com/RuTHlessBEat200/PatchMon-helm/issues
- Application repository: github.com/PatchMon/PatchMon
Nginx example configuration for PatchMon
This nginx configuration is for the type of installation where it's on bare-metal / native installation.
Edits the ports as required
# Example nginx config for PatchMon
# - Frontend served from disk; /bullboard and /api/ proxied to backend
# - HTTP → HTTPS redirect, WebSocket (WSS) support, static asset caching
# Replace: your-domain.com, /opt/your-domain.com/frontend, backend port
# Copy to /etc/nginx/sites-available/ and symlink from sites-enabled, then:
# sudo nginx -t && sudo systemctl reload nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream patchmon {
server 127.0.0.1:3001;
}
# Redirect all HTTP to HTTPS (so ws:// is never used; frontend uses wss://)
server {
listen 80;
listen [::]:80;
server_name your-domain.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com;
# SSL (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;
# Bull Board – queue UI and WebSocket (before location /)
location /bullboard {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Cookie $http_cookie;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 75s;
proxy_pass_header Set-Cookie;
proxy_cookie_path / /;
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
if ($request_method = 'OPTIONS') {
return 204;
}
}
# API – REST and WebSockets (SSH terminal, agent WS)
location /api/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
proxy_set_header X-Forwarded-Port 443;
proxy_cache_bypass $http_upgrade;
client_max_body_size 10m;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 75s;
}
# Health check
location /health {
proxy_pass http://localhost:3001/health;
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;
}
# Static assets caching – SPA js/css/images/fonts; exclude Bull Board and API
location ~* ^/(?!bullboard|api/).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /opt/your-domain.com/frontend;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Custom branding assets (logos, favicons) – from frontend build
location /assets/ {
alias /opt/your-domain.com/frontend/assets/;
expires 1h;
add_header Cache-Control "public, must-revalidate";
add_header Access-Control-Allow-Origin *;
}
# Frontend SPA
location / {
root /opt/your-domain.com/frontend;
try_files $uri $uri/ /index.html;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
}
# Optional: security.txt
# location /security.txt { return 301 https://$host/.well-known/security.txt; }
# location = /.well-known/security.txt { alias /var/www/html/.well-known/security.txt; }
}
Integrations
Integration API documentation (scoped credentials)
Overview
PatchMon's Integration API provides programmatic access to your PatchMon instance, enabling automation, integration with third-party tools, and custom workflows. API credentials use HTTP Basic Authentication with scoped permissions to control access to specific resources and actions.
Key Features
- Scoped Permissions: Fine-grained control over what each credential can access
- IP Restrictions: Optional IP allowlisting for enhanced security
- Expiration Dates: Set automatic expiration for temporary access
- Basic Authentication: Industry-standard authentication method (RFC 7617)
- Rate Limiting: Built-in protection against abuse
- Audit Trail: Track credential usage with last-used timestamps
Use Cases
- Automation: Integrate PatchMon data into CI/CD pipelines
- Inventory Management: Use with Ansible, Terraform, or other IaC tools
- Monitoring: Feed PatchMon data into monitoring dashboards
- Custom Scripts: Build custom tools that interact with PatchMon
- Third-Party Integrations: Connect PatchMon to other systems
Interactive API Reference (Swagger)
PatchMon includes a built-in interactive API reference powered by Swagger UI. You can explore all available endpoints, view request/response schemas, and test API calls directly from your browser.
To access the Swagger UI:
https://<your-patchmon-url>/api/v1/api-docs
Note: The Swagger UI requires you to be logged in to PatchMon (JWT authentication). Log in to your PatchMon dashboard first, then navigate to the URL above in the same browser session.
The Swagger reference covers all internal and scoped API endpoints. This documentation page focuses specifically on the scoped Integration API that uses Basic Authentication with API credentials.
Creating API Credentials
Step-by-Step Guide
1. Navigate to Settings
- Log in to your PatchMon instance as an administrator
- Go to Settings → Integrations
- You will see the Auto-Enrollment & API tab
2. Click "New Token"
Click the "New Token" button. A modal will appear where you can select the credential type.
3. Select "API" as the Usage Type
In the creation modal, select "API" as the usage type. This configures the credential for programmatic access via Basic Authentication.
4. Configure the Credential
Fill in the following fields:
Required Fields:
| Field | Description | Example |
|---|---|---|
| Token Name | A descriptive name for identification and audit purposes | Ansible Inventory, Monitoring Dashboard |
| Scopes | The permissions this credential should have (at least one required) | host: get |
Optional Fields:
| Field | Description | Example |
|---|---|---|
| Allowed IP Addresses | Comma-separated list of IPs or CIDR ranges that can use this credential. Leave empty for unrestricted access. | 192.168.1.100, 10.0.0.0/24 |
| Expiration Date | Automatic expiration date for the credential. Leave empty for no expiration. | 2026-12-31T23:59:59 |
| Default Host Group | Optionally assign a default host group | Production |
5. Save Your Credentials
⚠️ CRITICAL: Save these credentials immediately — the secret cannot be retrieved later!
After creation, a success modal displays:
- Token Key: The API key (used as the username in Basic Auth), prefixed with
patchmon_ae_ - Token Secret: The API secret (used as the password) — shown only once
- Granted Scopes: The permissions assigned
- Usage Examples: Pre-filled cURL commands ready to copy
Copy both the Token Key and Token Secret and store them securely before closing the modal.
Authentication
Basic Authentication
PatchMon API credentials use HTTP Basic Authentication as defined in RFC 7617.
Format
Authorization: Basic <base64(token_key:token_secret)>
How It Works
- Combine your token key and secret with a colon:
token_key:token_secret - Encode the combined string in Base64
- Prepend
Basicto the encoded string - Send it in the
Authorizationheader
Most HTTP clients handle this automatically — for example, cURL's -u flag or Python's HTTPBasicAuth.
Authentication Flow
┌─────────────┐ ┌─────────────┐
│ Client │ │ PatchMon │
│ Application │ │ Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. Send request with Basic Auth │
│ Authorization: Basic <base64> │
│───────────────────────────────────────────────>│
│ │
│ 2. Validate credentials │
│ a. Decode Base64 │
│ b. Find token by key │
│ c. Check is_active │
│ d. Check expiration │
│ e. Verify integration type │
│ f. Verify secret (bcrypt) │
│ g. Check IP restrictions │
│ │
│ 3. Validate scopes │
│ a. Check resource access │
│ b. Check action permission │
│ │
│ 4. Return response │
│<───────────────────────────────────────────────│
│ 200 OK + Data (if authorised) │
│ 401 Unauthorised (if auth fails) │
│ 403 Forbidden (if scope/IP check fails) │
│ │
│ 5. Update last_used_at │
│ timestamp │
Validation Steps (In Order)
The server performs these checks sequentially. If any step fails, the request is rejected immediately:
Available Scopes & Permissions
API credentials use a resource–action scope model:
{
"resource": ["action1", "action2"]
}
Host Resource
Resource name: host
| Action | Description |
|---|---|
get |
Read host data (list hosts, view details, stats, packages, network, system, reports, notes, integrations) |
put |
Replace host data |
patch |
Partially update host data |
update |
General update operations |
delete |
Delete hosts |
Example scope configurations:
// Read-only access
{ "host": ["get"] }
// Read and update
{ "host": ["get", "patch"] }
// Full access
{ "host": ["get", "put", "patch", "update", "delete"] }
Important Notes
- Scopes are explicit — no inheritance or wildcards. Each action must be explicitly granted.
getdoes not automatically includepatchor any other action.- At least one action must be granted for at least one resource. Credentials with no scopes will be rejected during creation.
API Endpoints
All endpoints are prefixed with /api/v1/api and require Basic Authentication with a credential that has the appropriate scope.
Endpoints Summary
| Endpoint | Method | Scope | Description |
|---|---|---|---|
/api/v1/api/hosts |
GET | host:get |
List all hosts with IP, groups, and optional stats |
/api/v1/api/hosts/:id/stats |
GET | host:get |
Get host package/repo statistics |
/api/v1/api/hosts/:id/info |
GET | host:get |
Get detailed host information |
/api/v1/api/hosts/:id/network |
GET | host:get |
Get host network configuration |
/api/v1/api/hosts/:id/system |
GET | host:get |
Get host system details |
/api/v1/api/hosts/:id/packages |
GET | host:get |
Get host packages (with optional update filter) |
/api/v1/api/hosts/:id/package_reports |
GET | host:get |
Get package update history |
/api/v1/api/hosts/:id/agent_queue |
GET | host:get |
Get agent queue status and jobs |
/api/v1/api/hosts/:id/notes |
GET | host:get |
Get host notes |
/api/v1/api/hosts/:id/integrations |
GET | host:get |
Get host integration status |
/api/v1/api/hosts/:id |
DELETE | host:delete |
Delete a host and all related data |
List Hosts
Retrieve a list of all hosts with their IP addresses and host group memberships. Optionally include package update statistics inline with each host.
Endpoint:
GET /api/v1/api/hosts
Required Scope: host:get
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
hostgroup |
string | No | Filter by host group name(s) or UUID(s). Comma-separated for multiple groups (OR logic). |
include |
string | No | Comma-separated list of additional data to include. Supported values: stats. |
Filtering by Host Groups:
# Filter by group name
GET /api/v1/api/hosts?hostgroup=Production
# Filter by multiple groups (hosts in ANY of the listed groups)
GET /api/v1/api/hosts?hostgroup=Production,Development
# Filter by group UUID
GET /api/v1/api/hosts?hostgroup=550e8400-e29b-41d4-a716-446655440000
# Mix names and UUIDs
GET /api/v1/api/hosts?hostgroup=Production,550e8400-e29b-41d4-a716-446655440000
Including Stats:
Use ?include=stats to add package update counts and additional host metadata to each host in a single request. This is more efficient than making separate /stats calls for every host.
# List all hosts with stats
GET /api/v1/api/hosts?include=stats
# Combine with host group filter
GET /api/v1/api/hosts?hostgroup=Production&include=stats
Note: If your host group names contain spaces, URL-encode them with
%20(e.g.Web%20Servers). Most HTTP clients handle this automatically.
Response (200 OK) — Without stats:
{
"hosts": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"friendly_name": "web-server-01",
"hostname": "web01.example.com",
"ip": "192.168.1.100",
"host_groups": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Production"
}
]
}
],
"total": 1,
"filtered_by_groups": ["Production"]
}
Response (200 OK) — With ?include=stats:
{
"hosts": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"friendly_name": "web-server-01",
"hostname": "web01.example.com",
"ip": "192.168.1.100",
"host_groups": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Production"
}
],
"os_type": "Ubuntu",
"os_version": "24.04 LTS",
"last_update": "2026-02-12T10:30:00.000Z",
"status": "active",
"needs_reboot": false,
"updates_count": 15,
"security_updates_count": 3,
"total_packages": 342
}
],
"total": 1,
"filtered_by_groups": ["Production"]
}
The
filtered_by_groupsfield is only present when ahostgroupfilter is applied.
Response Fields:
| Field | Type | Description |
|---|---|---|
hosts |
array | Array of host objects |
hosts[].id |
string (UUID) | Unique host identifier |
hosts[].friendly_name |
string | Human-readable host name |
hosts[].hostname |
string | System hostname |
hosts[].ip |
string | Primary IP address |
hosts[].host_groups |
array | Groups this host belongs to |
hosts[].os_type |
string | Operating system type (only with include=stats) |
hosts[].os_version |
string | Operating system version (only with include=stats) |
hosts[].last_update |
string (ISO 8601) | Timestamp of last agent update (only with include=stats) |
hosts[].status |
string | Host status, e.g. active, pending (only with include=stats) |
hosts[].needs_reboot |
boolean | Whether a reboot is pending (only with include=stats) |
hosts[].updates_count |
integer | Number of packages needing updates (only with include=stats) |
hosts[].security_updates_count |
integer | Number of security updates available (only with include=stats) |
hosts[].total_packages |
integer | Total installed packages (only with include=stats) |
total |
integer | Total number of hosts returned |
filtered_by_groups |
array | Groups used for filtering (only present when filtering) |
Get Host Statistics
Retrieve package and repository statistics for a specific host.
Endpoint:
GET /api/v1/api/hosts/:id/stats
Required Scope: host:get
Response (200 OK):
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"total_installed_packages": 342,
"outdated_packages": 15,
"security_updates": 3,
"total_repos": 8
}
Response Fields:
| Field | Type | Description |
|---|---|---|
host_id |
string (UUID) | The host identifier |
total_installed_packages |
integer | Total packages installed on this host |
outdated_packages |
integer | Packages that need updates |
security_updates |
integer | Packages with security updates available |
total_repos |
integer | Total repositories associated with the host |
Get Host Information
Retrieve detailed information about a specific host including OS details and host groups.
Endpoint:
GET /api/v1/api/hosts/:id/info
Required Scope: host:get
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"machine_id": "abc123def456",
"friendly_name": "web-server-01",
"hostname": "web01.example.com",
"ip": "192.168.1.100",
"os_type": "Ubuntu",
"os_version": "24.04 LTS",
"agent_version": "1.4.0",
"host_groups": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Production"
}
]
}
Get Host Network Information
Retrieve network configuration details for a specific host.
Endpoint:
GET /api/v1/api/hosts/:id/network
Required Scope: host:get
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ip": "192.168.1.100",
"gateway_ip": "192.168.1.1",
"dns_servers": ["8.8.8.8", "8.8.4.4"],
"network_interfaces": [
{
"name": "eth0",
"ip": "192.168.1.100",
"mac": "00:11:22:33:44:55"
}
]
}
Get Host System Information
Retrieve system-level information for a specific host including hardware, kernel, and reboot status.
Endpoint:
GET /api/v1/api/hosts/:id/system
Required Scope: host:get
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"architecture": "x86_64",
"kernel_version": "6.8.0-45-generic",
"installed_kernel_version": "6.8.0-50-generic",
"selinux_status": "disabled",
"system_uptime": "15 days, 3:22:10",
"cpu_model": "Intel Xeon E5-2680 v4",
"cpu_cores": 4,
"ram_installed": "8192 MB",
"swap_size": "2048 MB",
"load_average": {
"1min": 0.5,
"5min": 0.3,
"15min": 0.2
},
"disk_details": [
{
"filesystem": "/dev/sda1",
"size": "50G",
"used": "22G",
"available": "28G",
"use_percent": "44%",
"mounted_on": "/"
}
],
"needs_reboot": true,
"reboot_reason": "Kernel update pending"
}
Get Host Packages
Retrieve the list of packages installed on a specific host. Use the optional updates_only parameter to return only packages with available updates.
Endpoint:
GET /api/v1/api/hosts/:id/packages
Required Scope: host:get
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
updates_only |
string | No | — | Set to true to return only packages that need updates |
Examples:
# Get all packages for a host
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages
# Get only packages with available updates
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages?updates_only=true"
Response (200 OK):
{
"host": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"hostname": "web01.example.com",
"friendly_name": "web-server-01"
},
"packages": [
{
"id": "package-host-uuid",
"name": "nginx",
"description": "High performance web server",
"category": "web",
"current_version": "1.18.0-0ubuntu1.5",
"available_version": "1.24.0-2ubuntu1",
"needs_update": true,
"is_security_update": false,
"last_checked": "2026-02-12T10:30:00.000Z"
},
{
"id": "package-host-uuid-2",
"name": "openssl",
"description": "Secure Sockets Layer toolkit",
"category": "security",
"current_version": "3.0.2-0ubuntu1.14",
"available_version": "3.0.2-0ubuntu1.18",
"needs_update": true,
"is_security_update": true,
"last_checked": "2026-02-12T10:30:00.000Z"
}
],
"total": 2
}
Response Fields:
| Field | Type | Description |
|---|---|---|
host |
object | Basic host identification |
host.id |
string (UUID) | Host identifier |
host.hostname |
string | System hostname |
host.friendly_name |
string | Human-readable host name |
packages |
array | Array of package objects |
packages[].id |
string (UUID) | Host-package record identifier |
packages[].name |
string | Package name |
packages[].description |
string | Package description |
packages[].category |
string | Package category |
packages[].current_version |
string | Currently installed version |
packages[].available_version |
string | null | Available update version (null if up to date) |
packages[].needs_update |
boolean | Whether an update is available |
packages[].is_security_update |
boolean | Whether the available update is security-related |
packages[].last_checked |
string (ISO 8601) | When this package was last checked |
total |
integer | Total number of packages returned |
Tip: Packages are returned sorted by security updates first, then by update availability. This puts the most critical packages at the top.
Get Host Package Reports
Retrieve package update history reports for a specific host.
Endpoint:
GET /api/v1/api/hosts/:id/package_reports
Required Scope: host:get
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit |
integer | No | 10 | Maximum number of reports to return |
Response (200 OK):
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"reports": [
{
"id": "report-uuid",
"status": "success",
"date": "2026-02-12T10:30:00.000Z",
"total_packages": 342,
"outdated_packages": 15,
"security_updates": 3,
"payload_kb": 12.5,
"execution_time_seconds": 4.2,
"error_message": null
}
],
"total": 1
}
Get Host Agent Queue
Retrieve agent queue status and job history for a specific host.
Endpoint:
GET /api/v1/api/hosts/:id/agent_queue
Required Scope: host:get
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit |
integer | No | 10 | Maximum number of jobs to return |
Response (200 OK):
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"queue_status": {
"waiting": 0,
"active": 1,
"delayed": 0,
"failed": 0
},
"job_history": [
{
"id": "job-history-uuid",
"job_id": "bull-job-id",
"job_name": "package_update",
"status": "completed",
"attempt": 1,
"created_at": "2026-02-12T10:00:00.000Z",
"completed_at": "2026-02-12T10:05:00.000Z",
"error_message": null,
"output": null
}
],
"total_jobs": 1
}
Get Host Notes
Retrieve notes associated with a specific host.
Endpoint:
GET /api/v1/api/hosts/:id/notes
Required Scope: host:get
Response (200 OK):
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"notes": "Production web server. Enrolled via Proxmox auto-enrollment on 2026-01-15."
}
Get Host Integrations
Retrieve integration status and details for a specific host (e.g. Docker).
Endpoint:
GET /api/v1/api/hosts/:id/integrations
Required Scope: host:get
Response (200 OK) — Docker enabled:
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"integrations": {
"docker": {
"enabled": true,
"containers_count": 12,
"volumes_count": 5,
"networks_count": 3,
"description": "Monitor Docker containers, images, volumes, and networks. Collects real-time container status events."
}
}
}
Response (200 OK) — Docker not enabled:
{
"host_id": "550e8400-e29b-41d4-a716-446655440000",
"integrations": {
"docker": {
"enabled": false,
"description": "Monitor Docker containers, images, volumes, and networks. Collects real-time container status events."
}
}
}
Delete Host
Delete a specific host and all related data (cascade). This permanently removes the host and its associated packages, repositories, update history, Docker data, job history, and group memberships.
Endpoint:
DELETE /api/v1/api/hosts/:id
Required Scope: host:delete
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string (UUID) | Yes | The unique identifier of the host to delete |
Response (200 OK):
{
"message": "Host deleted successfully",
"deleted": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"friendly_name": "web-server-01",
"hostname": "web01.example.com"
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
message |
string | Confirmation message |
deleted.id |
string (UUID) | The ID of the deleted host |
deleted.friendly_name |
string | The friendly name of the deleted host |
deleted.hostname |
string | The hostname of the deleted host |
Error Responses:
| HTTP Code | Error | Description |
|---|---|---|
| 400 | Invalid host ID format |
The provided ID is not a valid UUID |
| 400 | Cannot delete host due to foreign key constraints |
The host has related data that prevents deletion |
| 404 | Host not found |
No host exists with the given ID |
| 403 | Access denied |
Credential does not have host:delete permission |
⚠️ Warning: This action is irreversible. All data associated with the host (packages, repositories, update history, Docker containers, job history, group memberships, etc.) will be permanently deleted.
Common Error Responses (All Endpoints)
404 Not Found — Host does not exist (for single-host endpoints):
{
"error": "Host not found"
}
500 Internal Server Error — Unexpected server error:
{
"error": "Failed to fetch hosts"
}
See the Troubleshooting section for authentication and permission errors.
Usage Examples
cURL Examples
List All Hosts
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts
List Hosts with Stats
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts?include=stats"
Filter by Host Group
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production"
Filter by Host Group with Stats
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production&include=stats"
Filter by Multiple Groups
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production,Development"
Get Host Statistics
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/stats
Get Host System Information
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/system
Get All Packages for a Host
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages
Delete a Host
curl -X DELETE -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts/HOST_UUID
Get Only Packages with Available Updates
curl -u "patchmon_ae_abc123:your_secret_here" \
"https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages?updates_only=true"
Pretty Print JSON Output
curl -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts | jq .
Python Examples
Using requests Library
import requests
from requests.auth import HTTPBasicAuth
# API credentials
API_KEY = "patchmon_ae_abc123"
API_SECRET = "your_secret_here"
BASE_URL = "https://patchmon.example.com"
# Create session with authentication
session = requests.Session()
session.auth = HTTPBasicAuth(API_KEY, API_SECRET)
# List all hosts
response = session.get(f"{BASE_URL}/api/v1/api/hosts")
if response.status_code == 200:
data = response.json()
print(f"Total hosts: {data['total']}")
for host in data['hosts']:
groups = ', '.join([g['name'] for g in host['host_groups']])
print(f" {host['friendly_name']} ({host['ip']}) — Groups: {groups}")
else:
print(f"Error: {response.status_code} — {response.json()}")
Filter by Host Group
# Filter by group name (requests handles URL encoding automatically)
response = session.get(
f"{BASE_URL}/api/v1/api/hosts",
params={"hostgroup": "Production"}
)
List Hosts with Inline Stats
# Get hosts with stats in a single request (more efficient than per-host /stats calls)
response = session.get(
f"{BASE_URL}/api/v1/api/hosts",
params={"include": "stats"}
)
if response.status_code == 200:
data = response.json()
for host in data['hosts']:
print(f"{host['friendly_name']}: {host['updates_count']} updates, "
f"{host['security_updates_count']} security, "
f"{host['total_packages']} total packages")
Get Host Packages (Updates Only)
# Get only packages that need updates for a specific host
response = session.get(
f"{BASE_URL}/api/v1/api/hosts/{host_id}/packages",
params={"updates_only": "true"}
)
if response.status_code == 200:
data = response.json()
print(f"Host: {data['host']['friendly_name']}")
print(f"Packages needing updates: {data['total']}")
for pkg in data['packages']:
security = " [SECURITY]" if pkg['is_security_update'] else ""
print(f" {pkg['name']}: {pkg['current_version']} → {pkg['available_version']}{security}")
Get Host Details and Stats
# First, get list of hosts
hosts_response = session.get(f"{BASE_URL}/api/v1/api/hosts")
hosts = hosts_response.json()['hosts']
# Then get stats for the first host
if hosts:
host_id = hosts[0]['id']
stats = session.get(f"{BASE_URL}/api/v1/api/hosts/{host_id}/stats").json()
print(f"Installed: {stats['total_installed_packages']}")
print(f"Outdated: {stats['outdated_packages']}")
print(f"Security: {stats['security_updates']}")
info = session.get(f"{BASE_URL}/api/v1/api/hosts/{host_id}/info").json()
print(f"OS: {info['os_type']} {info['os_version']}")
print(f"Agent: {info['agent_version']}")
Delete a Host
# Delete a host by UUID (requires host:delete scope)
host_id = "550e8400-e29b-41d4-a716-446655440000"
response = session.delete(f"{BASE_URL}/api/v1/api/hosts/{host_id}")
if response.status_code == 200:
data = response.json()
print(f"Deleted: {data['deleted']['friendly_name']} ({data['deleted']['hostname']})")
else:
print(f"Error: {response.status_code} — {response.json()}")
Error Handling
def get_hosts(hostgroup=None):
"""Get hosts with error handling."""
try:
params = {"hostgroup": hostgroup} if hostgroup else {}
response = session.get(
f"{BASE_URL}/api/v1/api/hosts",
params=params,
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
print("Authentication failed — check credentials")
elif e.response.status_code == 403:
print("Access denied — insufficient permissions")
else:
print(f"HTTP error: {e}")
return None
except requests.exceptions.Timeout:
print("Request timed out")
return None
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return None
Generate Ansible Inventory
import json
import requests
from requests.auth import HTTPBasicAuth
API_KEY = "patchmon_ae_abc123"
API_SECRET = "your_secret_here"
BASE_URL = "https://patchmon.example.com"
def generate_ansible_inventory():
"""Generate Ansible inventory from PatchMon hosts."""
auth = HTTPBasicAuth(API_KEY, API_SECRET)
response = requests.get(f"{BASE_URL}/api/v1/api/hosts", auth=auth, timeout=30)
if response.status_code != 200:
print(f"Error fetching hosts: {response.status_code}")
return
data = response.json()
inventory = {
"_meta": {"hostvars": {}},
"all": {"hosts": [], "children": []}
}
for host in data['hosts']:
hostname = host['friendly_name']
inventory["all"]["hosts"].append(hostname)
inventory["_meta"]["hostvars"][hostname] = {
"ansible_host": host['ip'],
"patchmon_id": host['id'],
"patchmon_hostname": host['hostname']
}
for group in host['host_groups']:
group_name = group['name'].lower().replace(' ', '_')
if group_name not in inventory:
inventory[group_name] = {"hosts": [], "vars": {}}
inventory["all"]["children"].append(group_name)
inventory[group_name]["hosts"].append(hostname)
print(json.dumps(inventory, indent=2))
if __name__ == "__main__":
generate_ansible_inventory()
JavaScript/Node.js Examples
Using Native fetch (Node.js 18+)
const API_KEY = 'patchmon_ae_abc123';
const API_SECRET = 'your_secret_here';
const BASE_URL = 'https://patchmon.example.com';
const authHeader = 'Basic ' + Buffer.from(`${API_KEY}:${API_SECRET}`).toString('base64');
async function getHosts(hostgroup = null) {
const url = new URL('/api/v1/api/hosts', BASE_URL);
if (hostgroup) {
url.searchParams.append('hostgroup', hostgroup);
}
const response = await fetch(url, {
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`HTTP ${response.status}: ${error.error}`);
}
return await response.json();
}
// List all hosts
getHosts()
.then(data => {
console.log(`Total: ${data.total}`);
data.hosts.forEach(host => {
console.log(`${host.friendly_name}: ${host.ip}`);
});
})
.catch(error => console.error('Error:', error.message));
Ansible Dynamic Inventory
Save this as patchmon_inventory.py and make it executable (chmod +x):
#!/usr/bin/env python3
"""
PatchMon Dynamic Inventory Script for Ansible.
Usage: ansible-playbook -i patchmon_inventory.py playbook.yml
"""
import json
import os
import sys
import requests
from requests.auth import HTTPBasicAuth
API_KEY = os.environ.get('PATCHMON_API_KEY')
API_SECRET = os.environ.get('PATCHMON_API_SECRET')
BASE_URL = os.environ.get('PATCHMON_URL', 'https://patchmon.example.com')
if not API_KEY or not API_SECRET:
print("Error: PATCHMON_API_KEY and PATCHMON_API_SECRET must be set", file=sys.stderr)
sys.exit(1)
def get_inventory():
auth = HTTPBasicAuth(API_KEY, API_SECRET)
try:
response = requests.get(f"{BASE_URL}/api/v1/api/hosts", auth=auth, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching inventory: {e}", file=sys.stderr)
sys.exit(1)
def build_ansible_inventory(patchmon_data):
inventory = {
"_meta": {"hostvars": {}},
"all": {"hosts": []}
}
groups = {}
for host in patchmon_data['hosts']:
hostname = host['friendly_name']
inventory["all"]["hosts"].append(hostname)
inventory["_meta"]["hostvars"][hostname] = {
"ansible_host": host['ip'],
"patchmon_id": host['id'],
"patchmon_hostname": host['hostname']
}
for group in host['host_groups']:
group_name = group['name'].lower().replace(' ', '_').replace('-', '_')
if group_name not in groups:
groups[group_name] = {
"hosts": [],
"vars": {"patchmon_group_id": group['id']}
}
groups[group_name]["hosts"].append(hostname)
inventory.update(groups)
return inventory
def main():
if len(sys.argv) == 2 and sys.argv[1] == '--list':
patchmon_data = get_inventory()
inventory = build_ansible_inventory(patchmon_data)
print(json.dumps(inventory, indent=2))
elif len(sys.argv) == 3 and sys.argv[1] == '--host':
print(json.dumps({}))
else:
print("Usage: patchmon_inventory.py --list", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
Usage:
export PATCHMON_API_KEY="patchmon_ae_abc123"
export PATCHMON_API_SECRET="your_secret_here"
export PATCHMON_URL="https://patchmon.example.com"
# Test inventory
./patchmon_inventory.py --list
# Use with ansible
ansible-playbook -i patchmon_inventory.py playbook.yml
ansible -i patchmon_inventory.py all -m ping
Security Best Practices
Credential Management
Do:
- Store credentials in a password manager or secrets vault (e.g. HashiCorp Vault, AWS Secrets Manager)
- Use environment variables for automation scripts
- Set expiration dates (recommended: 90 days)
- Grant only the minimum permissions needed (principle of least privilege)
- Rotate credentials regularly and delete old ones after migration
Don't:
- Hard-code credentials in source code
- Commit credentials to version control
- Share credentials via email or chat
- Store credentials in plain-text files
IP Restrictions
Restrict credentials to known IP addresses whenever possible:
Allowed IPs: 192.168.1.100, 10.0.0.0/24
For dynamic IPs, consider using a VPN with a static exit IP, a cloud NAT gateway, or a proxy server.
Network Security
- Always use HTTPS in production environments
- Verify SSL certificates — only disable verification (
-k) for development/testing - Use firewall rules to restrict PatchMon API access at the network level
Monitoring & Auditing
- Check "Last Used" timestamps regularly in the Integrations settings page
- Investigate credentials that have not been used in 30+ days
- Review all active credentials monthly
- Remove credentials for decommissioned systems
If Credentials Are Compromised
- Immediately disable the credential in PatchMon UI (Settings → Integrations → toggle off)
- Review the "Last Used" timestamp to understand the window of exposure
- Check server logs for any unauthorised access
- Create new credentials with a different scope if needed
- Delete the compromised credential after verification
- Notify your security team if sensitive data may have been accessed
Troubleshooting
Error Reference
| Error Message | HTTP Code | Cause | Solution |
|---|---|---|---|
Missing or invalid authorization header |
401 | No Authorization header, or it doesn't start with Basic |
Use -u key:secret with cURL, or set Authorization: Basic <base64> header |
Invalid credentials format |
401 | Base64-decoded value doesn't contain a colon separator | Check format is key:secret — ensure no extra characters |
Invalid API key |
401 | Token key not found in the database | Verify the credential exists in Settings → Integrations |
API key is disabled |
401 | Credential has been manually deactivated | Re-enable in Settings → Integrations, or create a new credential |
API key has expired |
401 | The expiration date has passed | Create a new credential to replace the expired one |
Invalid API key type |
401 | The credential's integration_type is not "api" |
Ensure you created the credential with the "API" usage type |
Invalid API secret |
401 | Secret doesn't match the stored bcrypt hash | Create a new credential (secrets cannot be retrieved) |
IP address not allowed |
403 | Client IP is not in the credential's allowed_ip_ranges |
Add your IP: curl https://ifconfig.me to find it |
Access denied — does not have permission to {action} {resource} |
403 | Credential is missing the required scope | Edit the credential and add the required permission |
Access denied — does not have access to {resource} |
403 | The resource is not included in the credential's scopes at all | Edit the credential's scopes to include the resource |
Host not found |
404 | The host UUID does not exist | Verify the UUID from the list hosts endpoint |
Invalid host ID format |
400 | The host ID is not a valid UUID (DELETE endpoint) | Ensure the ID is a valid UUID format |
Cannot delete host due to foreign key constraints |
400 | Host has related data preventing deletion | Check PatchMon server logs for details |
Failed to delete host |
500 | Unexpected error during host deletion | Check PatchMon server logs for details |
Failed to fetch hosts |
500 | Unexpected server error | Check PatchMon server logs for details |
Authentication failed |
500 | Unexpected error during authentication processing | Check PatchMon server logs; may indicate a database issue |
Debug Tips
cURL verbose mode:
curl -v -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts
Python debug logging:
import logging
logging.basicConfig(level=logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
Common Issues
Empty hosts array
- Verify hosts exist in PatchMon UI → Hosts page
- Check the
hostgroupfilter spelling matches exactly (case-sensitive) - Try listing all hosts without filters first to confirm API access works
Connection timeouts
# Test basic connectivity
ping patchmon.example.com
curl -I https://patchmon.example.com/health
SSL certificate errors
For development/testing with self-signed certificates:
curl -k -u "patchmon_ae_abc123:your_secret_here" \
https://patchmon.example.com/api/v1/api/hosts
For production, install a valid SSL certificate (e.g. Let's Encrypt).
Getting Help
If issues persist:
- Check PatchMon server logs for detailed error information
- Use the built-in Swagger UI to test endpoints interactively
- Search or create an issue at github.com/PatchMon/PatchMon
- Join the PatchMon community on Discord
Proxmox LXC Auto-Enrollment Guide
Overview
PatchMon's Proxmox Auto-Enrollment feature enables you to automatically discover and enroll LXC containers from your Proxmox hosts into PatchMon for centralized patch management. This eliminates manual host registration and ensures comprehensive coverage of your Proxmox infrastructure.
What It Does
- Automatically discovers running LXC containers on Proxmox hosts
- Bulk enrolls containers into PatchMon without manual intervention
- Installs agents inside each container automatically
- Assigns to host groups based on token configuration
- Tracks enrollment with full audit logging
Key Benefits
- Zero-Touch Enrollment - Run once, enroll all containers
- Secure by Design - Token-based authentication with hashed secrets
- Rate Limited - Prevents abuse with per-day host limits
- IP Restricted - Optional IP whitelisting for enhanced security
- Fully Auditable - Tracks who enrolled what and when
- Safe to Rerun - Already-enrolled containers are automatically skipped
Table of Contents
- How It Works
- Prerequisites
- Quick Start
- Step-by-Step Setup
- Usage Examples
- Configuration Options
- Security Best Practices
- Troubleshooting
- Advanced Usage
- API Reference
How It Works
Architecture Overview
┌─────────────────────┐
│ PatchMon Admin │
│ │
│ 1. Creates Token │
│ 2. Gets Key/Secret │
└──────────┬──────────┘
│
├─────────────────────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Proxmox Host │ │ PatchMon Server │
│ │ │ │
│ 3. Runs Script ────┼──────────▶ 4. Validates Token │
│ 4. Discovers LXCs │ │ 5. Creates Hosts │
│ 5. Gets Credentials│◀─────────┤ 6. Returns Creds │
│ 6. Installs Agents │ │ │
└──────────┬──────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ LXC Containers │
│ │
│ • curl installed │
│ • Agent installed │
│ • Reporting to PM │
└─────────────────────┘
Enrollment Process (Step by Step)
-
Admin creates auto-enrollment token in PatchMon UI
- Configures rate limits, IP restrictions, host group assignment
- Receives
token_keyandtoken_secret(shown only once!)
-
Admin runs enrollment script on Proxmox host
- Script authenticated with auto-enrollment token
- Discovers all running LXC containers using
pct list
-
For each container, the script:
- Gathers hostname, IP address, OS information, machine ID
- Calls PatchMon API to create host entry
- Receives unique
api_idandapi_keyfor that container - Uses
pct execto enter the container - Installs curl if missing
- Downloads and runs PatchMon agent installer
- Agent authenticates with container-specific credentials
-
Containers appear in PatchMon with full patch tracking enabled
Two-Tier Security Model
1. Auto-Enrollment Token (Script → PatchMon)
- Purpose: Create new host entries
- Scope: Limited to enrollment operations only
- Storage: Secret is hashed in database
- Lifespan: Reusable until revoked/expired
- Security: Rate limits + IP restrictions
2. Host API Credentials (Agent → PatchMon)
- Purpose: Report patches, send data, receive commands
- Scope: Per-host unique credentials
- Storage: API key is hashed (bcrypt) in database
- Lifespan: Permanent for that host
- Security: Host-specific, can be regenerated
Why This Matters:
- Compromised enrollment token ≠ compromised hosts
- Compromised host credential ≠ compromised enrollment
- Revoked enrollment token = no new enrollments (existing hosts unaffected)
- Lost credentials = create new token, don't affect existing infrastructure
Prerequisites
PatchMon Server Requirements
- PatchMon version with auto-enrollment support
- Admin user with "Manage Settings" permission
- Network accessible from Proxmox hosts
Proxmox Host Requirements
- Proxmox VE installed and running
- One or more LXC containers (VMs not supported)
- Root access to Proxmox host
- Network connectivity to PatchMon server
- Required commands:
pct,curl,jq,bash
Container Requirements
- Running state (stopped containers are skipped)
- Debian-based or RPM-based Linux distribution
- Network connectivity to PatchMon server
- Package manager (apt/yum/dnf) functional
Network Requirements
| Source | Destination | Port | Protocol | Purpose |
|---|---|---|---|---|
| Proxmox Host | PatchMon Server | 443 (HTTPS) | TCP | Enrollment API calls |
| LXC Containers | PatchMon Server | 443 (HTTPS) | TCP | Agent installation & reporting |
Firewall Notes:
- Outbound only connections (no inbound ports needed)
- HTTPS recommended (HTTP supported for internal networks)
- Self-signed certificates supported with
-kflag
Quick Start
1. Create Token (In PatchMon UI)
- Go to Settings → Integrations → Auto-Enrollment & API tab
- Click "New Token"
- Configure:
- Name: "Production Proxmox"
- Max Hosts/Day: 100
- Host Group: Select target group
- IP Restriction: Your Proxmox host IP
- Save credentials immediately (shown only once!)
2. One-Line Enrollment (On Proxmox Host)
curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=YOUR_KEY&token_secret=YOUR_SECRET" | bash
That's it! All running LXC containers will be enrolled and the PatchMon agent installed.
3. Verify in PatchMon
- Go to Hosts page
- See your containers listed with "pending" status
- Agent connects automatically after installation (usually within seconds)
- Status changes to "active" with package data
Step-by-Step Setup
Step 1: Create Auto-Enrollment Token
Via PatchMon Web UI
-
Log in to PatchMon as an administrator
-
Navigate to Settings
Dashboard → Settings → Integrations → Auto-Enrollment & API tab -
Click "New Token" button
-
Fill in token details:
Field Value Required Description Token Name Proxmox ProductionYes Descriptive name for this token Max Hosts Per Day 100Yes Rate limit (1-1000) Default Host Group Proxmox LXCNo Auto-assign enrolled hosts Allowed IP Addresses 192.168.1.10No Comma-separated IPs Expiration Date 2027-01-01No Auto-disable after date -
Click "Create Token"
-
CRITICAL: Save Credentials Now!
You'll see a success modal with:
Token Key: patchmon_ae_a1b2c3d4e5f6... Token Secret: 8f7e6d5c4b3a2f1e0d9c8b7a...Copy both values immediately! They cannot be retrieved later.
Pro Tip: Copy the one-line installation command shown in the modal - it has credentials pre-filled.
Step 2: Prepare Proxmox Host
Install Required Dependencies
# SSH to your Proxmox host
ssh root@proxmox-host
# Install jq (JSON processor)
apt-get update && apt-get install -y jq curl
# Verify installations
which pct jq curl
# Should show paths for all three commands
Download Enrollment Script
Method A: Direct Download from PatchMon (Recommended)
# Download with credentials embedded (copy from PatchMon UI)
curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=YOUR_KEY&token_secret=YOUR_SECRET" \
-o /root/proxmox_auto_enroll.sh
chmod +x /root/proxmox_auto_enroll.sh
Method B: Manual Configuration
# Download script template
cd /root
wget https://raw.githubusercontent.com/PatchMon/PatchMon/main/agents/proxmox_auto_enroll.sh
chmod +x proxmox_auto_enroll.sh
# Edit configuration
nano proxmox_auto_enroll.sh
# Update these lines:
PATCHMON_URL="https://patchmon.example.com"
AUTO_ENROLLMENT_KEY="patchmon_ae_your_key_here"
AUTO_ENROLLMENT_SECRET="your_secret_here"
Step 3: Test with Dry Run
Always test first!
# Dry run shows what would happen without making changes
DRY_RUN=true ./proxmox_auto_enroll.sh
Expected output:
[INFO] Found 5 LXC container(s)
[INFO] Processing LXC 100: webserver (status: running)
[INFO] [DRY RUN] Would enroll: proxmox-webserver
[INFO] Processing LXC 101: database (status: running)
[INFO] [DRY RUN] Would enroll: proxmox-database
...
[INFO] Successfully Enrolled: 5 (dry run)
Step 4: Run Actual Enrollment
# Enroll all containers
./proxmox_auto_enroll.sh
Monitor the output:
- Green
[SUCCESS]= Container enrolled and agent installed - Yellow
[WARN]= Container skipped (already enrolled or stopped) - Red
[ERROR]= Failure (check troubleshooting section)
Step 5: Verify in PatchMon
- Go to Hosts page in PatchMon UI
- Look for newly enrolled containers (names prefixed with "proxmox-")
- Initial status is "pending" (normal!)
- Agent connects automatically after installation (usually within seconds)
- Status changes to "active" with package data populated
Troubleshooting: If status stays "pending" after a couple of minutes, see Agent Not Reporting section.
Usage Examples
Basic Enrollment
# Enroll all running LXC containers
./proxmox_auto_enroll.sh
Dry Run Mode
# Preview what would be enrolled (no changes made)
DRY_RUN=true ./proxmox_auto_enroll.sh
Debug Mode
# Show detailed logging for troubleshooting
DEBUG=true ./proxmox_auto_enroll.sh
Custom Host Prefix
# Prefix container names (e.g., "prod-webserver" instead of "webserver")
HOST_PREFIX="prod-" ./proxmox_auto_enroll.sh
Include Stopped Containers
# Also process stopped containers (enrollment only, agent install fails)
SKIP_STOPPED=false ./proxmox_auto_enroll.sh
Force Install Mode (Broken Packages)
If containers have broken packages (CloudPanel, WHM, cPanel, etc.) that block apt-get:
# Bypass broken packages during agent installation
FORCE_INSTALL=true ./proxmox_auto_enroll.sh
Or use the force parameter when downloading:
curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET&force=true" | bash
What force mode does:
- Skips
apt-get updateif broken packages detected - Only installs missing critical tools (jq, curl, bc)
- Uses
--fix-broken --yesflags safely - Validates installations before proceeding
Scheduled Enrollment (Cron)
Automatically enroll new containers on a schedule. Since cron runs with a minimal environment (limited PATH, no user variables), you need to ensure the crontab has the correct environment set up for the script to find required commands like pct, curl, and jq.
Setting Up the Crontab
Edit the root crontab:
crontab -e
Add the following. The PATH and environment variables at the top are essential - without them the script will fail because cron does not inherit your shell's environment:
# === PatchMon Auto-Enrollment Environment ===
# Cron uses a minimal PATH by default (/usr/bin:/bin). The enrollment script
# requires pct, curl, and jq which may live in /usr/sbin or other paths.
# Set a full PATH so all commands are found.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Enrollment credentials (required by the script)
PATCHMON_URL=https://patchmon.example.com
AUTO_ENROLLMENT_KEY=patchmon_ae_your_key_here
AUTO_ENROLLMENT_SECRET=your_secret_here
# Optional overrides
# HOST_PREFIX=proxmox-
# FORCE_INSTALL=false
# CURL_FLAGS=-sk
# === Schedule ===
# Run daily at 2 AM
0 2 * * * /root/proxmox_auto_enroll.sh >> /var/log/patchmon-enroll.log 2>&1
# Or hourly for dynamic environments where containers are created frequently
# 0 * * * * /root/proxmox_auto_enroll.sh >> /var/log/patchmon-enroll.log 2>&1
Why This Matters
Cron does not load your interactive shell profile (~/.bashrc, ~/.profile, etc.). This means:
| What cron is missing | Impact | Fix |
|---|---|---|
PATH only includes /usr/bin:/bin |
pct not found (lives in /usr/sbin) |
Set PATH at top of crontab |
| No exported variables | PATCHMON_URL, credentials are empty |
Define them in crontab or use a wrapper |
| No TTY | Colour output codes may cause log clutter | Redirect to log file with 2>&1 |
Alternative: Wrapper Script
If you prefer not to put credentials in the crontab, create a wrapper script instead:
cat > /root/patchmon_enroll_cron.sh << 'EOF'
#!/bin/bash
# Wrapper that sets the environment for cron execution
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATCHMON_URL="https://patchmon.example.com"
export AUTO_ENROLLMENT_KEY="patchmon_ae_your_key_here"
export AUTO_ENROLLMENT_SECRET="your_secret_here"
# export HOST_PREFIX="proxmox-"
# export CURL_FLAGS="-sk"
/root/proxmox_auto_enroll.sh
EOF
chmod 700 /root/patchmon_enroll_cron.sh
Then reference the wrapper in crontab:
0 2 * * * /root/patchmon_enroll_cron.sh >> /var/log/patchmon-enroll.log 2>&1
Make sure the wrapper script is only readable by root (chmod 700) since it contains secrets.
Log Rotation
For long-running cron schedules, consider adding log rotation to prevent unbounded log growth:
cat > /etc/logrotate.d/patchmon-enroll << 'EOF'
/var/log/patchmon-enroll.log {
weekly
rotate 4
compress
missingok
notifempty
}
EOF
Verifying Cron is Working
# Check the cron job is registered
crontab -l | grep patchmon
# Check recent cron execution logs
grep patchmon /var/log/syslog | tail -n 20
# Check enrollment log output
tail -f /var/log/patchmon-enroll.log
Already-enrolled containers are automatically skipped on each run, so there is no risk of duplicates or errors from repeated execution.
Multi-Environment Setup
# Production environment (uses prod token)
export PATCHMON_URL="https://patchmon.example.com"
export AUTO_ENROLLMENT_KEY="patchmon_ae_prod_..."
export AUTO_ENROLLMENT_SECRET="prod_secret..."
export HOST_PREFIX="prod-"
./proxmox_auto_enroll.sh
# Development environment (uses dev token with different host group)
export AUTO_ENROLLMENT_KEY="patchmon_ae_dev_..."
export AUTO_ENROLLMENT_SECRET="dev_secret..."
export HOST_PREFIX="dev-"
./proxmox_auto_enroll.sh
Configuration Options
Environment Variables
All configuration can be set via environment variables:
| Variable | Default | Description | Example |
|---|---|---|---|
PATCHMON_URL |
Required | PatchMon server URL | https://patchmon.example.com |
AUTO_ENROLLMENT_KEY |
Required | Token key from PatchMon | patchmon_ae_abc123... |
AUTO_ENROLLMENT_SECRET |
Required | Token secret from PatchMon | def456ghi789... |
CURL_FLAGS |
-s |
Curl options | -sk (for self-signed SSL) |
DRY_RUN |
false |
Preview mode (no changes) | true/false |
HOST_PREFIX |
"" |
Prefix for host names | proxmox-, prod-, etc. |
SKIP_STOPPED |
true |
Skip stopped containers | true/false |
FORCE_INSTALL |
false |
Bypass broken packages | true/false |
DEBUG |
false |
Enable debug logging | true/false |
Script Configuration Section
Or edit the script directly:
# ===== CONFIGURATION =====
PATCHMON_URL="${PATCHMON_URL:-https://patchmon.example.com}"
AUTO_ENROLLMENT_KEY="${AUTO_ENROLLMENT_KEY:-your_key_here}"
AUTO_ENROLLMENT_SECRET="${AUTO_ENROLLMENT_SECRET:-your_secret_here}"
CURL_FLAGS="${CURL_FLAGS:--s}"
DRY_RUN="${DRY_RUN:-false}"
HOST_PREFIX="${HOST_PREFIX:-}"
SKIP_STOPPED="${SKIP_STOPPED:-true}"
FORCE_INSTALL="${FORCE_INSTALL:-false}"
Token Configuration (PatchMon UI)
Configure tokens in Settings → Integrations → Auto-Enrollment & API:
General Settings:
- Token Name: Descriptive identifier
- Active Status: Enable/disable without deleting
- Expiration Date: Auto-disable after date
Security Settings:
- Max Hosts Per Day: Rate limit (resets daily at midnight)
- Allowed IP Addresses: Comma-separated IP whitelist
- Default Host Group: Auto-assign enrolled hosts
Usage Statistics:
- Hosts Created Today: Current daily count
- Last Used: Timestamp of most recent enrollment
- Created By: Admin user who created token
- Created At: Token creation timestamp
Security Best Practices
Token Management
-
Store Securely
- Save credentials in password manager (1Password, LastPass, etc.)
- Never commit to version control
- Use environment variables or secure config management (Vault)
-
Principle of Least Privilege
- Create separate tokens for prod/dev/staging
- Use different tokens for different Proxmox clusters
- Set appropriate rate limits per environment
-
Regular Rotation
- Rotate tokens every 90 days
- Disable unused tokens immediately
- Monitor token usage for anomalies
-
IP Restrictions
- Always set
allowed_ip_rangesin production - Update if Proxmox host IPs change
- Use VPN/private network IPs when possible
- Always set
-
Expiration Dates
- Set expiration for temporary/testing tokens
- Review and extend before expiration
- Delete expired tokens to reduce attack surface
Network Security
-
Use HTTPS
- Always use encrypted connections in production
- Use valid SSL certificates (avoid
-kflag) - Self-signed OK for internal/testing environments
-
Network Segmentation
- Run enrollment over private network if possible
- Use proper firewall rules
- Restrict PatchMon server access to known IPs
Access Control
-
Admin Permissions
- Only admins with "Manage Settings" can create tokens
- Regular users cannot see token secrets
- Use role-based access control (RBAC)
-
Audit Logging
- Monitor token creation/deletion in PatchMon logs
- Track enrollment activity per token
- Review host notes for enrollment source
-
Container Security
- Ensure containers have minimal privileges
- Don't run enrollment as unprivileged user
- Use unprivileged containers where possible (enrollment still works)
Incident Response
If a token is compromised:
-
Immediately disable the token in PatchMon UI
- Settings → Integrations → Auto-Enrollment & API → Toggle "Disable"
-
Review recently enrolled hosts
- Check host notes for token name and enrollment date
- Verify all recent enrollments are legitimate
- Delete any suspicious hosts
-
Create new token
- Generate new credentials
- Update Proxmox script with new credentials
- Test enrollment with dry run
-
Investigate root cause
- How were credentials exposed?
- Update procedures to prevent recurrence
- Consider additional security measures
-
Delete old token
- After verifying new token works
- Document incident in change log
Troubleshooting
Common Errors and Solutions
Error: "pct command not found"
Symptom:
[ERROR] This script must run on a Proxmox host (pct command not found)
Cause: Script is running on a non-Proxmox machine
Solution:
# SSH to Proxmox host first
ssh root@proxmox-host
cd /root
./proxmox_auto_enroll.sh
Error: "Auto-enrollment credentials required"
Symptom:
[ERROR] Failed to enroll hostname - HTTP 401
Response: {"error":"Auto-enrollment credentials required"}
Cause: The X-Auto-Enrollment-Key and/or X-Auto-Enrollment-Secret headers are missing from the request
Solution:
- Verify the script has
AUTO_ENROLLMENT_KEYandAUTO_ENROLLMENT_SECRETset - Check for extra spaces/newlines in credentials
- Ensure token_key starts with
patchmon_ae_ - Regenerate token if credentials lost
# Test credentials manually
curl -X POST \
-H "X-Auto-Enrollment-Key: YOUR_KEY" \
-H "X-Auto-Enrollment-Secret: YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"friendly_name":"test","machine_id":"test"}' \
https://patchmon.example.com/api/v1/auto-enrollment/enroll
Error: "Invalid or inactive token" / "Invalid token secret"
Symptom:
[ERROR] Failed to enroll hostname - HTTP 401
Response: {"error":"Invalid or inactive token"}
or
[ERROR] Failed to enroll hostname - HTTP 401
Response: {"error":"Invalid token secret"}
Cause: Token key not found or disabled (Invalid or inactive token), or secret doesn't match (Invalid token secret), or token has expired (Token expired)
Solution:
- Check token status in PatchMon UI (Settings → Integrations)
- Enable if disabled
- Extend expiration if expired
- Verify the secret matches the one shown when the token was created
- Create new token if credentials are lost (secrets cannot be retrieved)
Error: "Rate limit exceeded"
Symptom:
[ERROR] Rate limit exceeded - maximum hosts per day reached
Cause: Token's max_hosts_per_day limit reached
Solution:
# Option 1: Wait until tomorrow (limit resets at midnight)
date
# Check current time, wait until 00:00
# Option 2: Increase limit in PatchMon UI
# Settings → Integrations → Edit Token → Max Hosts Per Day: 200
# Option 3: Create additional token for large enrollments
Error: "IP address not authorized"
Symptom:
[ERROR] Failed to enroll hostname - HTTP 403
Response: {"error":"IP address not authorized for this token"}
Cause: Proxmox host IP not in token's allowed_ip_ranges
Solution:
-
Find your Proxmox host IP:
ip addr show | grep 'inet ' | grep -v 127.0.0.1 -
Update token in PatchMon UI:
- Settings → Integrations → Edit Token
- Allowed IP Addresses: Add your IP
-
Or remove IP restriction entirely (not recommended for production)
Error: "jq: command not found"
Symptom:
[ERROR] Required command 'jq' not found. Please install it first.
Cause: Missing dependency
Solution:
# Debian/Ubuntu
apt-get update && apt-get install -y jq
# CentOS/RHEL
yum install -y jq
# Alpine
apk add --no-cache jq
Error: "Failed to install agent in container"
Symptom:
[WARN] Failed to install agent in container-name (exit: 1)
Install output: E: Unable to locate package curl
Cause: Agent installation failed inside LXC container
Solutions:
A. Network connectivity issue:
# Test from Proxmox host
pct exec 100 -- ping -c 3 patchmon.example.com
# Test from inside container
pct enter 100
curl -I https://patchmon.example.com
exit
B. Package manager issue:
# Enter container
pct enter 100
# Update package lists
apt-get update
# or
yum makecache
# Try manual agent install
curl https://patchmon.example.com/api/v1/hosts/install \
-H "X-API-ID: patchmon_xxx" \
-H "X-API-KEY: xxx" | bash
C. Unsupported OS:
- Agent supports: Ubuntu, Debian, CentOS, RHEL, Rocky Linux, AlmaLinux, Alpine
- Check
/etc/os-releasein container - Manually install on other distributions
D. Broken packages (use force mode):
FORCE_INSTALL=true ./proxmox_auto_enroll.sh
Error: SSL Certificate Problems
Symptom:
curl: (60) SSL certificate problem: self signed certificate
Cause: Self-signed certificate on PatchMon server
Solution:
# Use -k flag to skip certificate verification
export CURL_FLAGS="-sk"
./proxmox_auto_enroll.sh
Better solution: Install valid SSL certificate on PatchMon server using Let's Encrypt or corporate CA
Warning: Container Already Enrolled
Symptom:
[INFO] ✓ Host already enrolled and agent ping successful - skipping enrollment
Cause: The script detected an existing agent configuration (/etc/patchmon/config.yml and /etc/patchmon/credentials.yml) inside the container and the agent successfully pinged the PatchMon server.
This is normal! The script safely skips already-enrolled hosts. No action needed.
If you need to re-enroll:
- Delete host from PatchMon UI (Hosts page)
- Remove agent config inside the container:
pct exec <vmid> -- rm -rf /etc/patchmon/ - Rerun enrollment script
Agent Not Reporting
If containers show "pending" status after enrollment:
1. Check agent service is running:
pct enter 100
# For systemd-based containers
systemctl status patchmon-agent.service
# For OpenRC-based containers (Alpine)
rc-service patchmon-agent status
# For containers without init systems (crontab fallback)
ps aux | grep patchmon-agent
2. Check agent files exist:
ls -la /etc/patchmon/
# Should show: config.yml and credentials.yml
ls -la /usr/local/bin/patchmon-agent
# Should show the agent binary
3. Check agent logs:
# Systemd journal logs
journalctl -u patchmon-agent.service --no-pager -n 50
# Or check the agent log file
cat /etc/patchmon/logs/patchmon-agent.log
4. Test agent connectivity:
/usr/local/bin/patchmon-agent ping
# Should show success if credentials and connectivity are valid
5. Verify credentials:
cat /etc/patchmon/credentials.yml
# Should show api_id and api_key
cat /etc/patchmon/config.yml
# Should show patchmon_server URL
6. Restart the agent service:
# Systemd
systemctl restart patchmon-agent.service
# OpenRC
rc-service patchmon-agent restart
Debug Mode
Enable detailed logging:
DEBUG=true ./proxmox_auto_enroll.sh
Debug output includes:
- API request/response bodies
- Container command execution details
- Detailed error messages
- curl verbose output
Getting Help
If issues persist:
-
Check PatchMon server logs:
tail -f /path/to/patchmon/backend/logs/error.log -
Create GitHub issue with:
- PatchMon version
- Proxmox version
- Script output (redact credentials!)
- Debug mode output
- Server logs (if accessible)
-
Join Discord community for real-time support
Advanced Usage
Selective Enrollment
Enroll only specific containers:
# Only enroll containers 100-199
nano proxmox_auto_enroll.sh
# Add after line "while IFS= read -r line; do"
vmid=$(echo "$line" | awk '{print $1}')
if [[ $vmid -lt 100 ]] || [[ $vmid -gt 199 ]]; then
continue
fi
Or use container name filtering:
# Only enroll containers with "prod" in name
if [[ ! "$name" =~ prod ]]; then
continue
fi
Custom Host Naming
Advanced naming strategies:
# Include Proxmox node name
HOST_PREFIX="$(hostname)-"
# Result: proxmox01-webserver, proxmox02-database
# Include datacenter/location
HOST_PREFIX="dc1-"
# Result: dc1-webserver, dc1-database
# Include environment and node
HOST_PREFIX="prod-$(hostname | cut -d. -f1)-"
# Result: prod-px01-webserver
Multi-Node Proxmox Cluster
For Proxmox clusters with multiple nodes:
Option 1: Same token, different prefix per node
# On node 1
HOST_PREFIX="node1-" ./proxmox_auto_enroll.sh
# On node 2
HOST_PREFIX="node2-" ./proxmox_auto_enroll.sh
Option 2: Different tokens per node
- Create token for each node with different default host groups
- Node 1 → "Proxmox Node 1" group
- Node 2 → "Proxmox Node 2" group
Option 3: Centralized automation
#!/bin/bash
# central_enroll.sh
NODES=(
"root@proxmox01.example.com"
"root@proxmox02.example.com"
"root@proxmox03.example.com"
)
for node in "${NODES[@]}"; do
echo "Enrolling containers from $node..."
ssh "$node" "bash /root/proxmox_auto_enroll.sh"
done
Integration with Infrastructure as Code
Ansible Playbook:
---
- name: Enroll Proxmox LXC containers in PatchMon
hosts: proxmox_hosts
become: yes
tasks:
- name: Install dependencies
apt:
name:
- curl
- jq
state: present
- name: Download enrollment script
get_url:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key={{ token_key }}&token_secret={{ token_secret }}"
dest: /root/proxmox_auto_enroll.sh
mode: '0700'
- name: Run enrollment
command: /root/proxmox_auto_enroll.sh
register: enrollment_output
- name: Show enrollment results
debug:
var: enrollment_output.stdout_lines
Terraform (with null_resource):
resource "null_resource" "patchmon_enrollment" {
triggers = {
cluster_instance_ids = join(",", proxmox_lxc.containers.*.vmid)
}
provisioner "remote-exec" {
connection {
host = var.proxmox_host
user = "root"
private_key = file(var.ssh_key_path)
}
inline = [
"apt-get install -y jq",
"curl -s '${var.patchmon_url}/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=${var.token_key}&token_secret=${var.token_secret}' | bash"
]
}
}
Bulk API Enrollment
For very large deployments (100+ containers), use the bulk API endpoint directly:
#!/bin/bash
# bulk_enroll.sh
# Gather all container info
containers_json=$(pct list | tail -n +2 | while read -r line; do
vmid=$(echo "$line" | awk '{print $1}')
name=$(echo "$line" | awk '{print $3}')
echo "{\"friendly_name\":\"$name\",\"machine_id\":\"proxmox-lxc-$vmid\"}"
done | jq -s '.')
# Send bulk enrollment request
curl -X POST \
-H "X-Auto-Enrollment-Key: $AUTO_ENROLLMENT_KEY" \
-H "X-Auto-Enrollment-Secret: $AUTO_ENROLLMENT_SECRET" \
-H "Content-Type: application/json" \
-d "{\"hosts\":$containers_json}" \
"$PATCHMON_URL/api/v1/auto-enrollment/enroll/bulk"
Benefits:
- Single API call for all containers
- Faster for 50+ containers
- Partial success supported (individual failures don't block others)
Limitations:
- Max 50 hosts per request
- Does not install agents (must be done separately)
- Less detailed error reporting per host
Webhook-Triggered Enrollment
Trigger enrollment from PatchMon webhook (requires custom setup):
#!/bin/bash
# webhook_listener.sh
# Simple webhook listener
while true; do
# Listen for webhook on port 9000
nc -l -p 9000 -c 'echo -e "HTTP/1.1 200 OK\n\n"; /root/proxmox_auto_enroll.sh'
done
Then configure PatchMon (or monitoring system) to call webhook when conditions are met.
API Reference
Admin Endpoints (Authentication Required)
All admin endpoints require JWT authentication:
Authorization: Bearer <jwt_token>
Create Token
Endpoint: POST /api/v1/auto-enrollment/tokens
Request:
{
"token_name": "Proxmox Production",
"max_hosts_per_day": 100,
"default_host_group_id": "uuid",
"allowed_ip_ranges": ["192.168.1.10", "10.0.0.5"],
"expires_at": "2026-12-31T23:59:59Z",
"metadata": {
"integration_type": "proxmox-lxc",
"environment": "production"
}
}
Response: 201 Created
{
"message": "Auto-enrollment token created successfully",
"token": {
"id": "uuid",
"token_name": "Proxmox Production",
"token_key": "patchmon_ae_abc123...",
"token_secret": "def456...", // Only shown here!
"max_hosts_per_day": 100,
"default_host_group": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"created_by": {
"id": "uuid",
"username": "admin",
"first_name": "John",
"last_name": "Doe"
},
"expires_at": "2026-12-31T23:59:59Z"
},
"warning": "Save the token_secret now - it cannot be retrieved later!"
}
List Tokens
Endpoint: GET /api/v1/auto-enrollment/tokens
Response: 200 OK
[
{
"id": "uuid",
"token_name": "Proxmox Production",
"token_key": "patchmon_ae_abc123...",
"is_active": true,
"allowed_ip_ranges": ["192.168.1.10"],
"max_hosts_per_day": 100,
"hosts_created_today": 15,
"last_used_at": "2025-10-11T14:30:00Z",
"expires_at": "2026-12-31T23:59:59Z",
"created_at": "2025-10-01T10:00:00Z",
"default_host_group_id": "uuid",
"metadata": {"integration_type": "proxmox-lxc"},
"host_groups": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"users": {
"id": "uuid",
"username": "admin",
"first_name": "John",
"last_name": "Doe"
}
}
]
Get Token Details
Endpoint: GET /api/v1/auto-enrollment/tokens/:tokenId
Response: 200 OK (same structure as single token in list)
Update Token
Endpoint: PATCH /api/v1/auto-enrollment/tokens/:tokenId
Request:
{
"is_active": false,
"max_hosts_per_day": 200,
"allowed_ip_ranges": ["192.168.1.0/24"],
"expires_at": "2027-01-01T00:00:00Z"
}
Response: 200 OK
{
"message": "Token updated successfully",
"token": { /* updated token object */ }
}
Delete Token
Endpoint: DELETE /api/v1/auto-enrollment/tokens/:tokenId
Response: 200 OK
{
"message": "Auto-enrollment token deleted successfully",
"deleted_token": {
"id": "uuid",
"token_name": "Proxmox Production"
}
}
Enrollment Endpoints (Token Authentication)
Authentication via headers:
X-Auto-Enrollment-Key: patchmon_ae_abc123...
X-Auto-Enrollment-Secret: def456...
Download Enrollment Script
Endpoint: GET /api/v1/auto-enrollment/script
Query Parameters:
type(required): Script type (proxmox-lxcordirect-host)token_key(required): Auto-enrollment token keytoken_secret(required): Auto-enrollment token secretforce(optional):trueto enable force install mode
Example:
curl "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET&force=true"
Response: 200 OK (bash script with credentials injected)
Enroll Single Host
Endpoint: POST /api/v1/auto-enrollment/enroll
Request:
{
"friendly_name": "webserver",
"machine_id": "proxmox-lxc-100-abc123",
"metadata": {
"vmid": "100",
"proxmox_node": "proxmox01",
"ip_address": "10.0.0.10",
"os_info": "Ubuntu 22.04 LTS"
}
}
Response: 201 Created
{
"message": "Host enrolled successfully",
"host": {
"id": "uuid",
"friendly_name": "webserver",
"api_id": "patchmon_abc123",
"api_key": "def456ghi789",
"host_group": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"status": "pending"
}
}
Error Responses:
Note: The API does not perform duplicate host checks. Duplicate prevention is handled client-side by the enrollment script, which checks for an existing agent configuration inside each container before calling the API.
429 Too Many Requests - Rate limit exceeded:
{
"error": "Rate limit exceeded",
"message": "Maximum 100 hosts per day allowed for this token"
}
Bulk Enroll Hosts
Endpoint: POST /api/v1/auto-enrollment/enroll/bulk
Request:
{
"hosts": [
{
"friendly_name": "webserver",
"machine_id": "proxmox-lxc-100-abc123"
},
{
"friendly_name": "database",
"machine_id": "proxmox-lxc-101-def456"
}
]
}
Limits:
- Minimum: 1 host
- Maximum: 50 hosts per request
Response: 201 Created
{
"message": "Bulk enrollment completed: 2 succeeded, 0 failed, 0 skipped",
"results": {
"success": [
{
"id": "uuid",
"friendly_name": "webserver",
"api_id": "patchmon_abc123",
"api_key": "def456"
},
{
"id": "uuid",
"friendly_name": "database",
"api_id": "patchmon_ghi789",
"api_key": "jkl012"
}
],
"failed": [],
"skipped": []
}
}
FAQ
General Questions
Q: Can I use the same token for multiple Proxmox hosts?
A: Yes, as long as the combined enrollment count stays within max_hosts_per_day limit. Rate limits are per-token, not per-host.
Q: What happens if I run the script multiple times?
A: Already-enrolled containers are automatically skipped. The script checks for existing agent configuration inside each container and skips those where the agent is already installed and responsive. Safe to rerun!
Q: Can I enroll stopped LXC containers?
A: No, containers must be running. The script needs to execute commands inside the container to install the agent. Start containers before enrolling.
Q: Does this work with Proxmox VMs (QEMU)?
A: No, this script is LXC-specific and uses pct exec to enter containers. VMs require manual enrollment or a different automation approach (SSH-based).
Q: How do I unenroll a host?
A: Go to PatchMon UI → Hosts → Select host → Delete. The agent will stop reporting and the host record is removed from the database.
Q: Can I change the host group after enrollment?
A: Yes! In PatchMon UI → Hosts → Select host → Edit → Change host group.
Q: Can I see which hosts were enrolled by which token?
A: Yes, check the host "Notes" field in PatchMon. It includes the token name and enrollment timestamp.
Q: What if my Proxmox host IP address changes?
A: Update the token's allowed_ip_ranges in PatchMon UI (Settings → Integrations → Edit Token).
Q: Can I have multiple tokens with different host groups?
A: Yes! Create separate tokens for prod/dev/staging with different default host groups. Great for environment segregation.
Q: Is there a way to trigger enrollment from PatchMon GUI?
A: Not currently (would require inbound network access). The script must run on the Proxmox host. Future versions may support webhooks or agent-initiated enrollment.
Security Questions
Q: Are token secrets stored securely?
A: Yes, token secrets are hashed using bcrypt before storage. Only the hash is stored in the database, never the plain text.
Q: What happens if someone steals my auto-enrollment token?
A: They can create new hosts up to the rate limit, but cannot control existing hosts or access host data. Immediately disable the token in PatchMon UI if compromised.
Q: Can I audit who created which tokens?
A: Yes, each token stores the created_by_user_id. View in PatchMon UI or query the database.
Q: How does IP whitelisting work?
A: PatchMon checks the client IP from the HTTP request. If allowed_ip_ranges is configured, the IP must match one of the allowed ranges using CIDR notation (e.g., 192.168.1.0/24). Single IP addresses are also supported (e.g., 192.168.1.10).
Q: Can I use the same credentials for enrollment and agent communication?
A: No, they're separate. Auto-enrollment credentials create hosts. Each host gets unique API credentials for agent communication. This separation limits the blast radius of credential compromise.
Technical Questions
Q: Why does the agent require curl inside the container?
A: The agent script uses curl to communicate with PatchMon. The enrollment script automatically installs curl if missing.
Q: What Linux distributions are supported in containers?
A: Ubuntu, Debian, CentOS, RHEL, Rocky Linux, AlmaLinux, Alpine Linux. Any distribution with apt/yum/dnf/apk package managers.
Q: How much bandwidth does enrollment use?
A: Minimal. The script download is ~15KB, agent installation is ~50-100KB per container. Total: ~1-2MB for 10 containers.
Q: Can I run enrollment in parallel for faster processing?
A: Not recommended. The script processes containers sequentially to avoid overwhelming the PatchMon server. For 100+ containers, consider the bulk API endpoint.
Q: Does enrollment restart containers?
A: No, containers remain running. The agent is installed without reboots or service disruptions.
Q: What if the container doesn't have a hostname?
A: The script uses the container name from Proxmox as a fallback.
Q: Can I customize the agent installation?
A: Yes, modify the install_url in the enrollment script or use the PatchMon agent installation API parameters.
Troubleshooting Questions
Q: Why does enrollment fail with "dpkg was interrupted"?
A: Your container has broken packages. Use FORCE_INSTALL=true to bypass, or manually fix dpkg:
pct enter 100
dpkg --configure -a
apt-get install -f
Q: Why does the agent show "pending" status forever?
A: Agent likely can't reach PatchMon server. Check:
- Container network connectivity:
pct exec 100 -- ping patchmon.example.com - Agent service running:
pct exec 100 -- systemctl status patchmon-agent.service - Agent logs:
pct exec 100 -- journalctl -u patchmon-agent.service
Q: Can I test enrollment without actually creating hosts?
A: Yes, use dry run mode: DRY_RUN=true ./proxmox_auto_enroll.sh
Q: How do I get more verbose output?
A: Use debug mode: DEBUG=true ./proxmox_auto_enroll.sh
Support and Resources
Documentation
- PatchMon Documentation: https://docs.patchmon.net
- API Reference: https://docs.patchmon.net/api
- Agent Documentation: https://docs.patchmon.net/agent
Community
- Discord: https://patchmon.net/discord
- GitHub Issues: https://github.com/PatchMon/PatchMon/issues
- GitHub Discussions: https://github.com/PatchMon/PatchMon/discussions
Professional Support
For enterprise support, training, or custom integrations:
- Email: support@patchmon.net
- Website: https://patchmon.net/support
PatchMon Team
Auto-enrolment api documentation
Overview
This document provides comprehensive API documentation for PatchMon's auto-enrollment system, covering token management, host enrollment, and agent installation endpoints. These APIs enable automated device onboarding using tools like Ansible, Terraform, or custom scripts.
Table of Contents
- API Architecture
- Authentication
- Admin Endpoints
- Enrollment Endpoints
- Host Management Endpoints
- Ansible Integration Examples
- Error Handling
- Rate Limiting
- Security Considerations
API Architecture
Base URL Structure
https://your-patchmon-server.com/api/v1/
The API version is configurable via the API_VERSION environment variable (defaults to v1).
Endpoint Categories
| Category | Path Prefix | Authentication | Purpose |
|---|---|---|---|
| Admin | /auto-enrollment/tokens/* |
JWT (Bearer token) | Token management (CRUD) |
| Enrollment | /auto-enrollment/* |
Token key + secret (headers) | Host enrollment & script download |
| Host | /hosts/* |
API ID + key (headers) | Agent installation & data reporting |
Two-Tier Security Model
Tier 1: Auto-Enrollment Token
- Purpose: Create new host entries via enrollment
- Scope: Limited to enrollment operations only
- Authentication:
X-Auto-Enrollment-Key+X-Auto-Enrollment-Secretheaders - Rate Limited: Yes (configurable hosts per day per token)
- Storage: Secret is hashed (bcrypt) in the database
Tier 2: Host API Credentials
- Purpose: Agent communication (data reporting, updates, commands)
- Scope: Per-host unique credentials
- Authentication:
X-API-ID+X-API-KEYheaders - Rate Limited: No (per-host)
- Storage: API key is hashed (bcrypt) in the database
Why two tiers?
- Compromised enrollment token ≠ compromised hosts
- Compromised host credential ≠ compromised enrollment
- Revoking an enrollment token stops new enrollments without affecting existing hosts
Authentication
Admin Endpoints (JWT)
All admin endpoints require a valid JWT Bearer token from an authenticated user with "Manage Settings" permission:
curl -H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
https://your-patchmon-server.com/api/v1/auto-enrollment/tokens
Enrollment Endpoints (Token Key + Secret)
Enrollment endpoints authenticate via custom headers:
curl -H "X-Auto-Enrollment-Key: patchmon_ae_abc123..." \
-H "X-Auto-Enrollment-Secret: def456ghi789..." \
-H "Content-Type: application/json" \
https://your-patchmon-server.com/api/v1/auto-enrollment/enroll
Host Endpoints (API ID + Key)
Host endpoints authenticate via API credential headers:
curl -H "X-API-ID: patchmon_abc123" \
-H "X-API-KEY: def456ghi789" \
https://your-patchmon-server.com/api/v1/hosts/install
Admin Endpoints
All admin endpoints require JWT authentication and "Manage Settings" permission.
Create Auto-Enrollment Token
Endpoint: POST /api/v1/auto-enrollment/tokens
Request Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
token_name |
string | Yes | — | Descriptive name (max 255 chars) |
max_hosts_per_day |
integer | No | 100 |
Rate limit (1–1000) |
default_host_group_id |
string | No | null |
UUID of host group to auto-assign |
allowed_ip_ranges |
string[] | No | [] |
IP whitelist (exact IPs or CIDR notation) |
expires_at |
string | No | null |
ISO 8601 expiration date |
metadata |
object | No | {} |
Custom metadata (e.g. integration_type, environment) |
scopes |
object | No | null |
Permission scopes (only for API integration type tokens) |
Example Request:
{
"token_name": "Proxmox Production",
"max_hosts_per_day": 100,
"default_host_group_id": "uuid-of-host-group",
"allowed_ip_ranges": ["192.168.1.10", "10.0.0.0/24"],
"expires_at": "2026-12-31T23:59:59Z",
"metadata": {
"integration_type": "proxmox-lxc",
"environment": "production"
}
}
Response: 201 Created
{
"message": "Auto-enrollment token created successfully",
"token": {
"id": "uuid",
"token_name": "Proxmox Production",
"token_key": "patchmon_ae_abc123...",
"token_secret": "def456ghi789...",
"max_hosts_per_day": 100,
"default_host_group": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"created_by": {
"id": "uuid",
"username": "admin",
"first_name": "John",
"last_name": "Doe"
},
"expires_at": "2026-12-31T23:59:59Z",
"scopes": null
},
"warning": "⚠️ Save the token_secret now - it cannot be retrieved later!"
}
Important: The
token_secretis only returned in this response. It is hashed before storage and cannot be retrieved again.
List Auto-Enrollment Tokens
Endpoint: GET /api/v1/auto-enrollment/tokens
Response: 200 OK
[
{
"id": "uuid",
"token_name": "Proxmox Production",
"token_key": "patchmon_ae_abc123...",
"is_active": true,
"allowed_ip_ranges": ["192.168.1.10"],
"max_hosts_per_day": 100,
"hosts_created_today": 15,
"last_used_at": "2025-10-11T14:30:00Z",
"expires_at": "2026-12-31T23:59:59Z",
"created_at": "2025-10-01T10:00:00Z",
"default_host_group_id": "uuid",
"metadata": { "integration_type": "proxmox-lxc" },
"scopes": null,
"host_groups": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"users": {
"id": "uuid",
"username": "admin",
"first_name": "John",
"last_name": "Doe"
}
}
]
Tokens are returned in descending order by creation date. The token_secret is never included in list responses.
Get Token Details
Endpoint: GET /api/v1/auto-enrollment/tokens/{tokenId}
Response: 200 OK — Same structure as a single token in the list response (without token_secret).
Error: 404 Not Found if tokenId does not exist.
Update Token
Endpoint: PATCH /api/v1/auto-enrollment/tokens/{tokenId}
All fields are optional — only include fields you want to change.
Request Body:
| Field | Type | Description |
|---|---|---|
token_name |
string | Updated name (1–255 chars) |
is_active |
boolean | Enable or disable the token |
max_hosts_per_day |
integer | Updated rate limit (1–1000) |
allowed_ip_ranges |
string[] | Updated IP whitelist |
default_host_group_id |
string | Updated host group (set to empty string to clear) |
expires_at |
string | Updated expiration date (ISO 8601) |
scopes |
object | Updated scopes (API integration type tokens only) |
Example Request:
{
"is_active": false,
"max_hosts_per_day": 200,
"allowed_ip_ranges": ["192.168.1.0/24"]
}
Response: 200 OK
{
"message": "Token updated successfully",
"token": {
"id": "uuid",
"token_name": "Proxmox Production",
"token_key": "patchmon_ae_abc123...",
"is_active": false,
"max_hosts_per_day": 200,
"allowed_ip_ranges": ["192.168.1.0/24"],
"host_groups": { "id": "uuid", "name": "Proxmox LXC", "color": "#3B82F6" },
"users": { "id": "uuid", "username": "admin", "first_name": "John", "last_name": "Doe" }
}
}
Errors:
404 Not Found— Token does not exist400 Bad Request— Host group not found, or scopes update attempted on a non-API token
Delete Token
Endpoint: DELETE /api/v1/auto-enrollment/tokens/{tokenId}
Response: 200 OK
{
"message": "Auto-enrollment token deleted successfully",
"deleted_token": {
"id": "uuid",
"token_name": "Proxmox Production"
}
}
Error: 404 Not Found if tokenId does not exist.
Enrollment Endpoints
Download Enrollment Script
Endpoint: GET /api/v1/auto-enrollment/script
This endpoint validates the token credentials, then serves a bash script with the PatchMon server URL, token credentials, and configuration injected automatically.
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Script type: proxmox-lxc or direct-host |
token_key |
Yes | Auto-enrollment token key |
token_secret |
Yes | Auto-enrollment token secret |
force |
No | Set to true to enable force install mode (for broken packages) |
Example:
curl "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET"
Response: 200 OK — Plain text bash script with credentials injected.
Errors:
400 Bad Request— Missing or invalidtypeparameter401 Unauthorized— Missing credentials, invalid/inactive token, invalid secret, or expired token404 Not Found— Script file not found on server
Enroll Single Host
Endpoint: POST /api/v1/auto-enrollment/enroll
Headers:
X-Auto-Enrollment-Key: patchmon_ae_abc123...
X-Auto-Enrollment-Secret: def456ghi789...
Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
friendly_name |
string | Yes | Display name for the host (max 255 chars) |
machine_id |
string | No | Unique machine identifier (max 255 chars) |
metadata |
object | No | Additional metadata (vmid, proxmox_node, ip_address, os_info, etc.) |
Example Request:
{
"friendly_name": "webserver",
"machine_id": "proxmox-lxc-100-abc123",
"metadata": {
"vmid": "100",
"proxmox_node": "proxmox01",
"ip_address": "10.0.0.10",
"os_info": "Ubuntu 22.04 LTS"
}
}
Response: 201 Created
{
"message": "Host enrolled successfully",
"host": {
"id": "uuid",
"friendly_name": "webserver",
"api_id": "patchmon_abc123def456",
"api_key": "raw-api-key-value",
"host_group": {
"id": "uuid",
"name": "Proxmox LXC",
"color": "#3B82F6"
},
"status": "pending"
}
}
Note: The
api_keyis only returned in this response (plain text). It is hashed before storage. Thehost_groupisnullif no default host group is configured on the token.
Error Responses:
| Status | Error | Cause |
|---|---|---|
400 |
Validation errors | Missing or invalid friendly_name |
401 |
Auto-enrollment credentials required |
Missing X-Auto-Enrollment-Key or X-Auto-Enrollment-Secret headers |
401 |
Invalid or inactive token |
Token key not found or token is disabled |
401 |
Invalid token secret |
Secret does not match |
401 |
Token expired |
Token has passed its expiration date |
403 |
IP address not authorized for this token |
Client IP not in allowed_ip_ranges |
429 |
Rate limit exceeded |
Token's max_hosts_per_day limit reached |
Duplicate handling: The API does not perform server-side duplicate host checks. Duplicate prevention is handled client-side by the enrollment script, which checks for an existing agent configuration (
/etc/patchmon/config.yml) inside each container before calling the API.
Bulk Enroll Hosts
Endpoint: POST /api/v1/auto-enrollment/enroll/bulk
Headers:
X-Auto-Enrollment-Key: patchmon_ae_abc123...
X-Auto-Enrollment-Secret: def456ghi789...
Content-Type: application/json
Request Body:
{
"hosts": [
{
"friendly_name": "webserver",
"machine_id": "proxmox-lxc-100-abc123"
},
{
"friendly_name": "database",
"machine_id": "proxmox-lxc-101-def456"
}
]
}
Limits:
- Minimum: 1 host per request
- Maximum: 50 hosts per request
- Each host must have a
friendly_name(required);machine_idis optional
Response: 201 Created
{
"message": "Bulk enrollment completed: 2 succeeded, 0 failed, 0 skipped",
"results": {
"success": [
{
"id": "uuid",
"friendly_name": "webserver",
"api_id": "patchmon_abc123",
"api_key": "def456"
},
{
"id": "uuid",
"friendly_name": "database",
"api_id": "patchmon_ghi789",
"api_key": "jkl012"
}
],
"failed": [],
"skipped": []
}
}
Rate Limit Error (429):
{
"error": "Rate limit exceeded",
"message": "Only 5 hosts remaining in daily quota"
}
The bulk endpoint checks the remaining daily quota before processing. If the number of hosts in the request exceeds the remaining quota, the entire request is rejected.
Host Management Endpoints
These endpoints are used by the PatchMon agent (not the enrollment script). They authenticate using the per-host X-API-ID and X-API-KEY credentials returned during enrollment.
Download Agent Installation Script
Endpoint: GET /api/v1/hosts/install
Serves a shell script that bootstraps the PatchMon agent on a host. The script uses a secure bootstrap token mechanism — actual API credentials are not embedded directly in the script.
Headers:
X-API-ID: patchmon_abc123
X-API-KEY: def456ghi789
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
force |
No | Set to true to enable force install mode |
arch |
No | Architecture override (e.g. amd64, arm64); auto-detected if omitted |
Response: 200 OK — Plain text shell script with bootstrap token injected.
Download Agent Binary/Script
Endpoint: GET /api/v1/hosts/agent/download
Downloads the PatchMon agent binary (Go binary for modern agents) or migration script (for legacy bash agents).
Headers:
X-API-ID: patchmon_abc123
X-API-KEY: def456ghi789
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
arch |
No | Architecture (e.g. amd64, arm64) |
force |
No | Set to binary to force binary download |
Response: 200 OK — Binary file or shell script.
Host Data Update
Endpoint: POST /api/v1/hosts/update
Used by the agent to report package data, system information, and hardware details.
Headers:
X-API-ID: patchmon_abc123
X-API-KEY: def456ghi789
Content-Type: application/json
Request Body Fields:
| Field | Type | Required | Description |
|---|---|---|---|
packages |
array | Yes | Array of package objects (max 10,000) |
packages[].name |
string | Yes | Package name |
packages[].currentVersion |
string | Yes | Currently installed version |
packages[].availableVersion |
string | No | Available update version |
packages[].needsUpdate |
boolean | Yes | Whether an update is available |
packages[].isSecurityUpdate |
boolean | No | Whether the update is security-related |
agentVersion |
string | No | Reporting agent version |
osType |
string | No | Operating system type |
osVersion |
string | No | Operating system version |
hostname |
string | No | System hostname |
ip |
string | No | System IP address |
architecture |
string | No | CPU architecture |
cpuModel |
string | No | CPU model name |
cpuCores |
integer | No | Number of CPU cores |
ramInstalled |
float | No | Installed RAM in GB |
swapSize |
float | No | Swap size in GB |
diskDetails |
array | No | Array of disk objects |
gatewayIp |
string | No | Default gateway IP |
dnsServers |
array | No | Array of DNS server IPs |
networkInterfaces |
array | No | Array of network interface objects |
kernelVersion |
string | No | Running kernel version |
installedKernelVersion |
string | No | Installed (on-disk) kernel version |
selinuxStatus |
string | No | SELinux status (enabled, disabled, or permissive) |
systemUptime |
string | No | System uptime |
loadAverage |
array | No | Load average values |
machineId |
string | No | Machine ID |
needsReboot |
boolean | No | Whether a reboot is required |
rebootReason |
string | No | Reason a reboot is required |
repositories |
array | No | Configured package repositories |
executionTime |
string | No | Time taken to gather data |
Example Request:
{
"packages": [
{
"name": "nginx",
"currentVersion": "1.18.0",
"availableVersion": "1.20.0",
"needsUpdate": true,
"isSecurityUpdate": false
}
],
"agentVersion": "1.2.3",
"cpuModel": "Intel Xeon E5-2680 v4",
"cpuCores": 8,
"ramInstalled": 16.0,
"swapSize": 2.0,
"diskDetails": [
{
"device": "/dev/sda1",
"mountPoint": "/",
"size": "50GB",
"used": "25GB",
"available": "25GB"
}
],
"gatewayIp": "192.168.1.1",
"dnsServers": ["8.8.8.8", "8.8.4.4"],
"networkInterfaces": [
{
"name": "eth0",
"ip": "192.168.1.10",
"mac": "00:11:22:33:44:55"
}
],
"kernelVersion": "5.4.0-74-generic",
"selinuxStatus": "disabled"
}
Response: 200 OK
{
"message": "Host updated successfully",
"packagesProcessed": 1,
"updatesAvailable": 1,
"securityUpdates": 0
}
Ansible Integration Examples
Basic Playbook for Proxmox Enrollment
---
- name: Enroll Proxmox LXC containers in PatchMon
hosts: proxmox_hosts
become: yes
vars:
patchmon_url: "https://patchmon.example.com"
token_key: "{{ vault_patchmon_token_key }}"
token_secret: "{{ vault_patchmon_token_secret }}"
host_prefix: "prod-"
tasks:
- name: Install dependencies
apt:
name:
- curl
- jq
state: present
- name: Download enrollment script
get_url:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key={{ token_key }}&token_secret={{ token_secret }}"
dest: /root/proxmox_auto_enroll.sh
mode: '0700'
- name: Run enrollment
command: /root/proxmox_auto_enroll.sh
environment:
HOST_PREFIX: "{{ host_prefix }}"
DEBUG: "true"
register: enrollment_output
- name: Show enrollment results
debug:
var: enrollment_output.stdout_lines
Advanced Playbook with Token Management
---
- name: Manage PatchMon Proxmox Integration
hosts: localhost
vars:
patchmon_url: "https://patchmon.example.com"
admin_token: "{{ vault_patchmon_admin_token }}"
tasks:
- name: Create Proxmox enrollment token
uri:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/tokens"
method: POST
headers:
Authorization: "Bearer {{ admin_token }}"
Content-Type: "application/json"
body_format: json
body:
token_name: "{{ inventory_hostname }}-proxmox"
max_hosts_per_day: 200
default_host_group_id: "{{ proxmox_host_group_id }}"
allowed_ip_ranges: ["{{ proxmox_host_ip }}"]
expires_at: "2026-12-31T23:59:59Z"
metadata:
integration_type: "proxmox-lxc"
environment: "{{ environment }}"
status_code: 201
register: token_response
- name: Store token credentials
set_fact:
enrollment_token_key: "{{ token_response.json.token.token_key }}"
enrollment_token_secret: "{{ token_response.json.token.token_secret }}"
- name: Deploy enrollment script to Proxmox hosts
include_tasks: deploy_enrollment.yml
vars:
enrollment_token_key: "{{ enrollment_token_key }}"
enrollment_token_secret: "{{ enrollment_token_secret }}"
Playbook for Bulk Enrollment via API
---
- name: Bulk enroll Proxmox containers
hosts: proxmox_hosts
become: yes
vars:
patchmon_url: "https://patchmon.example.com"
token_key: "{{ vault_patchmon_token_key }}"
token_secret: "{{ vault_patchmon_token_secret }}"
tasks:
- name: Get LXC container list
shell: |
pct list | tail -n +2 | while read -r line; do
vmid=$(echo "$line" | awk '{print $1}')
name=$(echo "$line" | awk '{print $3}')
status=$(echo "$line" | awk '{print $2}')
if [ "$status" = "running" ]; then
machine_id=$(pct exec "$vmid" -- bash -c "cat /etc/machine-id 2>/dev/null || cat /var/lib/dbus/machine-id 2>/dev/null || echo 'proxmox-lxc-$vmid-'$(cat /proc/sys/kernel/random/uuid)" 2>/dev/null || echo "proxmox-lxc-$vmid-unknown")
echo "{\"friendly_name\":\"$name\",\"machine_id\":\"$machine_id\"}"
fi
done | jq -s '.'
register: containers_json
- name: Bulk enroll containers
uri:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/enroll/bulk"
method: POST
headers:
X-Auto-Enrollment-Key: "{{ token_key }}"
X-Auto-Enrollment-Secret: "{{ token_secret }}"
Content-Type: "application/json"
body_format: json
body:
hosts: "{{ containers_json.stdout | from_json }}"
status_code: 201
register: enrollment_result
- name: Display enrollment results
debug:
msg: "{{ enrollment_result.json.message }}"
Ansible Role
# roles/patchmon_proxmox/tasks/main.yml
---
- name: Install PatchMon dependencies
package:
name:
- curl
- jq
state: present
- name: Create PatchMon directory
file:
path: /opt/patchmon
state: directory
mode: '0755'
- name: Download enrollment script
get_url:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key={{ token_key }}&token_secret={{ token_secret }}&force={{ force_install | default('false') }}"
dest: /opt/patchmon/proxmox_auto_enroll.sh
mode: '0700'
- name: Run enrollment script
command: /opt/patchmon/proxmox_auto_enroll.sh
environment:
PATCHMON_URL: "{{ patchmon_url }}"
AUTO_ENROLLMENT_KEY: "{{ token_key }}"
AUTO_ENROLLMENT_SECRET: "{{ token_secret }}"
HOST_PREFIX: "{{ host_prefix | default('') }}"
DRY_RUN: "{{ dry_run | default('false') }}"
DEBUG: "{{ debug | default('false') }}"
FORCE_INSTALL: "{{ force_install | default('false') }}"
register: enrollment_output
- name: Display enrollment results
debug:
var: enrollment_output.stdout_lines
when: enrollment_output.stdout_lines is defined
- name: Fail if enrollment had errors
fail:
msg: "Enrollment failed with errors"
when: enrollment_output.rc != 0
Ansible Vault for Credentials
# group_vars/all/vault.yml (encrypted with ansible-vault)
---
vault_patchmon_admin_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
vault_patchmon_token_key: "patchmon_ae_abc123..."
vault_patchmon_token_secret: "def456ghi789..."
Playbook with Error Handling and Retries
---
- name: Robust Proxmox enrollment with error handling
hosts: proxmox_hosts
become: yes
vars:
patchmon_url: "https://patchmon.example.com"
token_key: "{{ vault_patchmon_token_key }}"
token_secret: "{{ vault_patchmon_token_secret }}"
max_retries: 3
retry_delay: 30
tasks:
- name: Test PatchMon connectivity
uri:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/tokens"
method: GET
headers:
Authorization: "Bearer {{ vault_patchmon_admin_token }}"
status_code: 200
retries: "{{ max_retries }}"
delay: "{{ retry_delay }}"
- name: Download enrollment script
get_url:
url: "{{ patchmon_url }}/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key={{ token_key }}&token_secret={{ token_secret }}"
dest: /root/proxmox_auto_enroll.sh
mode: '0700'
retries: "{{ max_retries }}"
delay: "{{ retry_delay }}"
- name: Run enrollment with retry logic
shell: |
for i in {1..{{ max_retries }}}; do
echo "Attempt $i of {{ max_retries }}"
if /root/proxmox_auto_enroll.sh; then
echo "Enrollment successful"
exit 0
else
echo "Enrollment failed, retrying in {{ retry_delay }} seconds..."
sleep {{ retry_delay }}
fi
done
echo "All enrollment attempts failed"
exit 1
register: enrollment_result
- name: Handle enrollment failure
fail:
msg: "Proxmox enrollment failed after {{ max_retries }} attempts"
when: enrollment_result.rc != 0
- name: Parse enrollment results
set_fact:
enrolled_count: "{{ enrollment_result.stdout | regex_search('Successfully Enrolled:\\s+(\\d+)', '\\1') | default('0') }}"
failed_count: "{{ enrollment_result.stdout | regex_search('Failed:\\s+(\\d+)', '\\1') | default('0') }}"
- name: Report enrollment statistics
debug:
msg: |
Enrollment completed:
- Successfully enrolled: {{ enrolled_count }} containers
- Failed: {{ failed_count }} containers
Error Handling
HTTP Status Codes
| Code | Meaning | When It Occurs |
|---|---|---|
200 |
OK | Successful read/update operations |
201 |
Created | Token or host created successfully |
400 |
Bad Request | Validation errors, invalid host group, invalid script type |
401 |
Unauthorized | Missing, invalid, or expired credentials |
403 |
Forbidden | IP address not in token's whitelist |
404 |
Not Found | Token or resource not found |
429 |
Too Many Requests | Token's daily host creation limit exceeded |
500 |
Internal Server Error | Unexpected server error |
Error Response Formats
Simple error:
{
"error": "Error message describing what went wrong"
}
Error with detail:
{
"error": "Rate limit exceeded",
"message": "Maximum 100 hosts per day allowed for this token"
}
Validation errors (400):
{
"errors": [
{
"msg": "Token name is required (max 255 characters)",
"param": "token_name",
"location": "body"
}
]
}
Rate Limiting
Token-Based Rate Limits
Each auto-enrollment token has a configurable max_hosts_per_day limit:
- Default: 100 hosts per day per token
- Range: 1–1000 hosts per day
- Reset: Daily (when the first request of a new day is received)
- Scope: Per-token, not per-IP
When the limit is exceeded, the API returns 429 Too Many Requests:
{
"error": "Rate limit exceeded",
"message": "Maximum 100 hosts per day allowed for this token"
}
For bulk enrollment, the remaining daily quota is checked against the request size. If the request contains more hosts than the remaining quota allows, the entire request is rejected:
{
"error": "Rate limit exceeded",
"message": "Only 5 hosts remaining in daily quota"
}
Global Rate Limiting
The auto-enrollment endpoints are also subject to the server's global authentication rate limiter, which applies to all authentication-related endpoints.
Security Considerations
Token Security
- Secret hashing: Token secrets are hashed with bcrypt (cost factor 10) before storage
- One-time display: Secrets are only returned during token creation
- Rotation: Recommended every 90 days
- Scope limitation: Tokens can only create hosts — they cannot read, modify, or delete existing host data
IP Restrictions
Tokens support IP whitelisting with both exact IPs and CIDR notation:
{
"allowed_ip_ranges": ["192.168.1.10", "10.0.0.0/24"]
}
IPv4-mapped IPv6 addresses (e.g. ::ffff:192.168.1.10) are automatically handled.
Host API Key Security
- Host API keys (
api_key) are hashed with bcrypt before storage - The installation script uses a bootstrap token mechanism — the actual API credentials are not embedded in the script
- Bootstrap tokens are single-use and expire after 5 minutes
Network Security
- Always use HTTPS in production
- The
ignore_ssl_self_signedserver setting automatically configures curl flags in served scripts - Implement firewall rules to restrict PatchMon server access to known IPs
Audit Trail
All enrollment activity is logged:
- Token name included in host notes (e.g. "Auto-enrolled via Production Proxmox on 2025-10-11T14:30:00Z")
- Token creation tracks
created_by_user_id last_used_attimestamp updated on each enrollment
Complete Endpoint Summary
Admin Endpoints (JWT Authentication)
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/auto-enrollment/tokens |
Create token |
GET |
/api/v1/auto-enrollment/tokens |
List all tokens |
GET |
/api/v1/auto-enrollment/tokens/{tokenId} |
Get single token |
PATCH |
/api/v1/auto-enrollment/tokens/{tokenId} |
Update token |
DELETE |
/api/v1/auto-enrollment/tokens/{tokenId} |
Delete token |
Enrollment Endpoints (Token Authentication)
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/auto-enrollment/script?type=... |
Download enrollment script |
POST |
/api/v1/auto-enrollment/enroll |
Enroll single host |
POST |
/api/v1/auto-enrollment/enroll/bulk |
Bulk enroll hosts (max 50) |
Host Endpoints (API Credentials)
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/hosts/install |
Download installation script |
GET |
/api/v1/hosts/agent/download |
Download agent binary/script |
POST |
/api/v1/hosts/update |
Report host data |
Quick Reference: curl Examples
Create a token:
curl -X POST \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"token_name": "Production Proxmox",
"max_hosts_per_day": 100,
"default_host_group_id": "uuid",
"allowed_ip_ranges": ["192.168.1.10"]
}' \
https://patchmon.example.com/api/v1/auto-enrollment/tokens
Download and run enrollment script:
curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET" | bash
Enroll a host directly:
curl -X POST \
-H "X-Auto-Enrollment-Key: patchmon_ae_abc123..." \
-H "X-Auto-Enrollment-Secret: def456ghi789..." \
-H "Content-Type: application/json" \
-d '{
"friendly_name": "webserver",
"machine_id": "proxmox-lxc-100-abc123"
}' \
https://patchmon.example.com/api/v1/auto-enrollment/enroll
Download agent installation script:
curl -H "X-API-ID: patchmon_abc123" \
-H "X-API-KEY: def456ghi789" \
https://patchmon.example.com/api/v1/hosts/install | bash
Integration Patterns
Pattern 1: Script-Based (Simplest)
# Download and execute in one command — credentials are injected into the script
curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET" | bash
Pattern 2: API-First (Most Control)
# 1. Create token via admin API
# 2. Enroll hosts via enrollment API (single or bulk)
# 3. Download agent scripts using per-host API credentials
# 4. Install agents with host-specific credentials
Pattern 3: Hybrid (Recommended for Automation)
# 1. Create token via admin API (or UI)
# 2. Download enrollment script with token embedded
# 3. Distribute and run script on Proxmox hosts
# 4. Script handles both enrollment and agent installation
Ansible Dynamic Library
Github Repo : https://github.com/PatchMon/PatchMon-ansible/tree/main
A dynamic inventory plugin for Ansible that queries the PatchMon HTTP JSON API and exposes hosts as an Ansible inventory.
Description
The dynamic_inventory plugin allows you to use PatchMon as a dynamic inventory source for Ansible. It queries the PatchMon API to retrieve host information including hostnames, IP addresses, and group assignments, and automatically generates an Ansible inventory.
Requirements
- Ansible: >= 2.19.0
- Python: 3.6+
- Dependencies:
requests >= 2.25.1
Installation
Install from Ansible Galaxy
ansible-galaxy collection install patchmon.dynamic_inventory
Install from Source
-
Clone the repository:
git clone https://github.com/PatchMon/PatchMon-ansible.git cd PatchMon-ansible/patchmon/dynamic_inventory -
Build the collection:
ansible-galaxy collection build -
Install the collection:
ansible-galaxy collection install patchmon-dynamic_inventory-*.tar.gz -
Install dependencies:
pip install -r requirements.txt
Configuration
Create an inventory configuration file (e.g., patchmon_inventory.yml):
---
plugin: patchmon.dynamic_inventory
api_url: http://localhost:3000/api/v1/api/hosts/
api_key: your_api_key
api_secret: your_api_secret
verify_ssl: false
Configuration Options
| Option | Description | Required | Default |
|---|---|---|---|
plugin |
Name of the plugin | ✅ | patchmon.dynamic_inventory |
api_url |
URL of the PatchMon API endpoint that returns JSON host data | ✅ | — |
api_key |
API key for authentication | ✅ | — |
api_secret |
API secret for authentication | ✅ | — |
verify_ssl |
Whether to verify SSL certificates when contacting the API | ❌ | true |
Usage
Basic Usage
Run Ansible commands with the inventory file:
# List all hosts
ansible-inventory -i patchmon_inventory.yml --list
# Ping all hosts
ansible all -i patchmon_inventory.yml -m ping
# Run a playbook
ansible-playbook -i patchmon_inventory.yml playbook.yml
Configure as Default Inventory
Add to your ansible.cfg:
[defaults]
inventory = patchmon_inventory.yml
[inventory]
enable_plugins = patchmon.dynamic_inventory.dynamic_inventory
Using in Playbooks
Create a playbook (e.g., ping.yml):
---
- name: Test connectivity to all hosts
hosts: all
gather_facts: no
tasks:
- name: Ping hosts
ansible.builtin.ping:
Run the playbook:
ansible-playbook ping.yml
API Response Format
The plugin expects the PatchMon API to return JSON in the following format:
{
"hosts": [
{
"hostname": "server1.example.com",
"ip": "192.168.1.10",
"host_groups": [
{
"name": "web_servers"
},
{
"name": "production"
}
]
},
{
"hostname": "server2.example.com",
"ip": "192.168.1.11",
"host_groups": [
{
"name": "db_servers"
},
{
"name": "production"
}
]
}
]
}
Inventory Mapping
- Hostname: The
hostnamefield is used as the Ansible host name - IP Address: The
ipfield is mapped to theansible_hostvariable - Groups: Each entry in
host_groupscreates an Ansible group, and hosts are assigned to these groups
Examples
Example 1: List Inventory
ansible-inventory -i patchmon_inventory.yml --list
Output:
{
"_meta": {
"hostvars": {
"server1.example.com": {
"ansible_host": "192.168.1.10"
},
"server2.example.com": {
"ansible_host": "192.168.1.11"
}
}
},
"all": {
"children": [
"ungrouped",
"web_servers",
"db_servers",
"production"
]
},
"db_servers": {
"hosts": [
"server2.example.com"
]
},
"production": {
"hosts": [
"server1.example.com",
"server2.example.com"
]
},
"web_servers": {
"hosts": [
"server1.example.com"
]
}
}
Example 2: Target Specific Groups
# Run on web servers only
ansible-playbook -i patchmon_inventory.yml playbook.yml --limit web_servers
# Run on production hosts only
ansible-playbook -i patchmon_inventory.yml playbook.yml --limit production
Example 3: Using Environment Variables
For security, you can use Ansible vault or environment variables:
---
plugin: patchmon.dynamic_inventory
api_url: http://localhost:3000/api/v1/api/hosts/
api_key: "{{ lookup('env', 'PATCHMON_API_KEY') }}"
api_secret: "{{ lookup('env', 'PATCHMON_API_SECRET') }}"
verify_ssl: false
Authentication
The plugin uses HTTP Basic Authentication with the provided api_key and api_secret. Make sure these credentials have the necessary permissions to query the PatchMon API.
SSL Verification
By default, SSL certificate verification is enabled (verify_ssl: true). For development or self-signed certificates, you can disable it by setting verify_ssl: false. Note: Disabling SSL verification is not recommended for production environments.
Troubleshooting
Test API Connectivity
# Test the API endpoint directly
curl -u "api_key:api_secret" http://localhost:3000/api/v1/api/hosts/
Debug Inventory
# Show detailed inventory information
ansible-inventory -i patchmon_inventory.yml --list --debug
# Test with verbose output
ansible-inventory -i patchmon_inventory.yml --list -v
Common Issues
- Authentication Errors: Verify that your
api_keyandapi_secretare correct - Connection Errors: Check that the
api_urlis accessible and the API is running - JSON Parsing Errors: Ensure the API returns valid JSON in the expected format
- Missing Hosts: Verify that the API response contains a
hostsarray
Development
Testing
Test the plugin locally:
# Test inventory parsing
ansible-inventory -i patchmon_inventory.yml --list
# Test with a playbook
ansible-playbook -i patchmon_inventory.yml ping.yml
Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
License
AGPL-3.0-or-later
See the LICENSE file for details.
Authors
- Steve Libonati stevelibonati@yahoo.com
Links
- Repository: https://github.com/PatchMon/PatchMon-ansible
- Issues: https://github.com/PatchMon/PatchMon-ansible/issues
GetHomepage Integration Guide
Overview
PatchMon provides a seamless integration with GetHomepage (formerly Homepage), allowing you to display real-time PatchMon statistics in your GetHomepage dashboard. This integration uses authenticated API endpoints to securely fetch and display your patch management data.
Features
Default Widget Display
By default, the GetHomepage widget displays:
- Total Hosts - Number of active monitored hosts
- Hosts Needing Updates - Hosts with outdated packages
- Security Updates - Number of security-related updates available
Additional Available Data
The API provides additional metrics that you can display by customizing the widget mappings:
- Up-to-Date Hosts - Hosts with no pending updates
- Total Outdated Packages - Aggregate count of packages needing updates
- Total Repositories - Number of active repositories
- Recent Updates (24h) - Update activity in the last 24 hours
- Hosts with Security Updates - Number of hosts requiring security patches
- OS Distribution - Breakdown of operating systems across hosts (returned in API but requires custom formatting)
Prerequisites
- PatchMon instance running and accessible
- GetHomepage installed and configured
- Network access from GetHomepage to PatchMon
- HTTPS recommended (but HTTP works with fallback clipboard)
Setup Instructions
Step 1: Create an API Key in PatchMon
- Log in to PatchMon as an administrator
- Navigate to Settings → Integrations
- Click on the GetHomepage tab
- Click "New API Key" button
- Fill in the token details:
- Token Name: A descriptive name (e.g., "GetHomepage Widget")
- Allowed IP Addresses (Optional): Restrict access to specific IPs
- Expiration Date (Optional): Set an expiration if needed
- Click "Create Token"
Step 2: Save Your Credentials
After creating the token, you'll see a success modal with:
- Token Key: Your API username
- Token Secret: Your API password (shown only once!)
- Base64 Encoded Credentials: Pre-encoded for convenience
- Complete Widget Configuration: Ready-to-use YAML
⚠️ Important: Save the token secret immediately. You won't be able to view it again!
Step 3: Configure GetHomepage
Method A: Copy Complete Configuration (Recommended)
- In the PatchMon success modal, click "Copy Config" button
- Open your GetHomepage
services.ymlfile - Paste the copied configuration
- Save the file
- Restart GetHomepage
The default configuration displays 3 key metrics:
- PatchMon:
href: http://your-patchmon-url:3000
description: PatchMon Statistics
icon: http://your-patchmon-url:3000/assets/favicon.svg
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <base64_encoded_credentials>
mappings:
- field: total_hosts
label: Total Hosts
- field: hosts_needing_updates
label: Needs Updates
- field: security_updates
label: Security Updates
Note: You can add more fields to the
mappingssection. See Configuration Options below for all available fields.
Method B: Manual Configuration
If you need to manually create the base64 credentials:
-
Encode your credentials:
echo -n "YOUR_API_KEY:YOUR_API_SECRET" | base64 -
Create the widget configuration in
services.yml:- PatchMon: href: http://your-patchmon-url:3000 description: PatchMon Statistics icon: http://your-patchmon-url:3000/assets/favicon.svg widget: type: customapi url: http://your-patchmon-url:3000/api/v1/gethomepage/stats headers: Authorization: Basic <your_base64_credentials> mappings: - field: total_hosts label: Total Hosts - field: hosts_needing_updates label: Needs Updates - field: security_updates label: Security Updates -
Restart GetHomepage:
docker restart gethomepage # or systemctl restart gethomepage
Configuration Options
Widget Mappings
The default widget configuration displays 3 metrics: Total Hosts, Hosts Needing Updates, and Security Updates.
You can customize which statistics to display by adding or removing fields in the mappings section. The API provides 8 numeric metrics you can choose from.
How to Customize Mappings
- Locate the
mappings:section in your GetHomepageservices.yml - Add or remove field entries - each entry has two parts:
field:- The metric name from the API (see table below)label:- How it appears in GetHomepage (customize as you like)
- You can display up to ~6-8 metrics before the widget becomes crowded
- Save and restart GetHomepage to see changes
Available Fields
| Field | Description | Default |
|---|---|---|
total_hosts |
Total number of active hosts | ✅ Yes |
hosts_needing_updates |
Hosts with outdated packages | ✅ Yes |
security_updates |
Number of security updates available | ✅ Yes |
up_to_date_hosts |
Hosts with no pending updates | ❌ No |
total_outdated_packages |
Total outdated packages across all hosts | ❌ No |
hosts_with_security_updates |
Hosts requiring security updates | ❌ No |
total_repos |
Number of active repositories | ❌ No |
recent_updates_24h |
Successful updates in last 24 hours | ❌ No |
top_os_1_count |
Count of most common OS (e.g., "Ubuntu: 20") | ❌ No |
top_os_2_count |
Count of 2nd most common OS | ❌ No |
top_os_3_count |
Count of 3rd most common OS | ❌ No |
Note: Fields marked with ❌ are available but not included in the default configuration. Add them to your
mappingssection to display them.OS Distribution: The API also returns
top_os_1_name,top_os_2_name, andtop_os_3_name(strings like "Ubuntu", "Debian", "Rocky Linux"). However, GetHomepage widgets display these awkwardly. It's better to use just the count fields with custom labels that include the OS name (see examples below).
Quick Start: Adding a Metric
Example: Add "Recent Updates (24h)" to your widget
Before (Default - 3 metrics):
mappings:
- field: total_hosts
label: Total Hosts
- field: hosts_needing_updates
label: Needs Updates
- field: security_updates
label: Security Updates
After (Custom - 4 metrics):
mappings:
- field: total_hosts
label: Total Hosts
- field: hosts_needing_updates
label: Needs Updates
- field: security_updates
label: Security Updates
- field: recent_updates_24h # ← Added this line
label: Updated (24h) # ← And this line
Result: Your widget now shows 4 metrics including recent update activity.
You can add any combination of the 8 available fields. Just ensure the field: name matches exactly as shown in the table above.
Advanced Mapping Examples
Example: Security-Focused Widget
Shows security-critical metrics only:
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: security_updates
label: Security Patches
- field: hosts_with_security_updates
label: Hosts at Risk
- field: hosts_needing_updates
label: Total Pending
Example: Repository Management Widget
Focus on repository and host counts:
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: total_repos
label: Repositories
- field: total_hosts
label: Managed Hosts
- field: up_to_date_hosts
label: Up-to-Date
Example: Activity Monitoring Widget
Track recent update activity:
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: recent_updates_24h
label: Updated (24h)
- field: hosts_needing_updates
label: Pending Updates
- field: up_to_date_hosts
label: Fully Patched
Example: Package-Focused Widget
Monitor outdated packages:
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: total_outdated_packages
label: Outdated Packages
- field: security_updates
label: Security Updates
- field: hosts_needing_updates
label: Affected Hosts
Example: OS Distribution Widget
Show your infrastructure breakdown by operating system:
widget:
type: customapi
url: http://your-patchmon-url:3000/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: total_hosts
label: Total Hosts
- field: top_os_1_count
label: Ubuntu Hosts # Customize these labels based on your actual OS mix
- field: top_os_2_count
label: Debian Hosts
- field: top_os_3_count
label: Rocky Linux Hosts
Pro Tip: First test the endpoint with
curlto see what your actual top 3 operating systems are, then customize the labels accordingly. The API returns the OS names intop_os_1_name,top_os_2_name, andtop_os_3_name.
Custom Icon
By default, the widget uses PatchMon's favicon. You can customize it:
# Use PatchMon's dark logo
icon: http://your-patchmon-url:3000/assets/logo_dark.png
# Use PatchMon's light logo
icon: http://your-patchmon-url:3000/assets/logo_light.png
# Use GetHomepage's built-in icons
icon: server
# Use a local icon in GetHomepage
icon: /icons/patchmon.png
API Endpoint Details
Endpoint
GET /api/v1/gethomepage/stats
Authentication
- Type: HTTP Basic Authentication
- Format:
Authorization: Basic <base64(key:secret)>
Response Format
The endpoint returns JSON with the following structure:
{
"total_hosts": 42,
"total_outdated_packages": 156,
"total_repos": 12,
"hosts_needing_updates": 15,
"up_to_date_hosts": 27,
"security_updates": 23,
"hosts_with_security_updates": 8,
"recent_updates_24h": 34,
"os_distribution": [
{ "name": "Ubuntu", "count": 20 },
{ "name": "Debian", "count": 12 },
{ "name": "Rocky Linux", "count": 10 }
],
"top_os_1_name": "Ubuntu",
"top_os_1_count": 20,
"top_os_2_name": "Debian",
"top_os_2_count": 12,
"top_os_3_name": "Rocky Linux",
"top_os_3_count": 10,
"last_updated": "2025-10-11T12:34:56.789Z"
}
All Available Metrics Explained
All numeric fields can be used in GetHomepage mappings:
| Field | Type | Description | Use Case |
|---|---|---|---|
total_hosts |
Number | Total active hosts in PatchMon | Overall infrastructure size |
hosts_needing_updates |
Number | Hosts with at least one outdated package | Hosts requiring attention |
up_to_date_hosts |
Number | Hosts with zero outdated packages | Compliant/healthy hosts |
security_updates |
Number | Total security updates available across all hosts | Critical patches needed |
hosts_with_security_updates |
Number | Hosts requiring security patches | High-risk hosts |
total_outdated_packages |
Number | Sum of all outdated packages | Total patching workload |
total_repos |
Number | Active repositories being monitored | Repository coverage |
recent_updates_24h |
Number | Successful updates in last 24 hours | Recent patching activity |
top_os_1_name |
String | Name of most common OS | OS breakdown |
top_os_1_count |
Number | Count of most common OS | OS breakdown |
top_os_2_name |
String | Name of 2nd most common OS | OS breakdown |
top_os_2_count |
Number | Count of 2nd most common OS | OS breakdown |
top_os_3_name |
String | Name of 3rd most common OS | OS breakdown |
top_os_3_count |
Number | Count of 3rd most common OS | OS breakdown |
os_distribution |
Array | Full breakdown of OS types (for advanced use) | Infrastructure composition |
last_updated |
String (ISO 8601) | Timestamp of when stats were generated | Data freshness |
Note: The API provides top 3 OS distribution data as flat fields (
top_os_*) that can be easily displayed in GetHomepage widgets. The fullos_distributionarray is also available for custom integrations.
Health Check Endpoint
GET /api/v1/gethomepage/health
Returns basic health status and API key name.
Managing API Keys
View Existing Keys
- Go to Settings → Integrations → GetHomepage
- View all created API keys with:
- Token name
- Creation date
- Last used timestamp
- Active/Inactive status
- Expiration date (if set)
Disable/Enable Keys
Click the "Disable" or "Enable" button on any API key to toggle its status.
Delete Keys
Click the trash icon to permanently delete an API key. This action cannot be undone.
Security Features
- IP Restrictions: Limit API key usage to specific IP addresses
- Expiration Dates: Set automatic expiration for temporary access
- Last Used Tracking: Monitor when keys are being used
- One-Time Secret Display: Token secrets are only shown once at creation
Troubleshooting
Error: "Missing or invalid authorization header"
Cause: GetHomepage isn't sending the Authorization header correctly.
Solution:
- Verify the
headers:section is properly indented inservices.yml - Ensure base64 credentials are correctly encoded
- Check for extra spaces or line breaks in the configuration
- Verify you're using
type: customapi(not another widget type)
Error: "Invalid API key"
Cause: The API key doesn't exist or was deleted.
Solution:
- Verify the API key exists in PatchMon (Settings → Integrations)
- Create a new API key if needed
- Update GetHomepage configuration with new credentials
Error: "API key is disabled"
Cause: The API key has been disabled in PatchMon.
Solution:
- Go to Settings → Integrations → GetHomepage
- Click "Enable" on the API key
Error: "API key has expired"
Cause: The API key has passed its expiration date.
Solution:
- Create a new API key without expiration
- Or create a new key with a future expiration date
- Update GetHomepage configuration
Error: "IP address not allowed"
Cause: GetHomepage's IP address is not in the allowed list.
Solution:
- Check GetHomepage's IP address
- Update the API key's allowed IP ranges in PatchMon
- Or remove IP restrictions if not needed
Widget Not Showing Data
Checklist:
- GetHomepage can reach PatchMon URL (test with
curl) - API key is active and not expired
- Base64 credentials are correct
-
services.ymlsyntax is valid YAML - GetHomepage has been restarted after config changes
- Check GetHomepage logs for error messages
Testing the API Endpoint
Test the endpoint manually to see all available metrics:
# Step 1: Encode your credentials
echo -n "your_key:your_secret" | base64
# Output: eW91cl9rZXk6eW91cl9zZWNyZXQ=
# Step 2: Test the endpoint with your credentials
curl -H "Authorization: Basic YOUR_BASE64_CREDENTIALS" \
http://your-patchmon-url:3000/api/v1/gethomepage/stats
Expected response: JSON with all 8 core metrics plus OS distribution:
{
"total_hosts": 42,
"hosts_needing_updates": 15,
"security_updates": 23,
"up_to_date_hosts": 27,
"total_outdated_packages": 156,
"hosts_with_security_updates": 8,
"total_repos": 12,
"recent_updates_24h": 34,
"top_os_1_name": "Ubuntu",
"top_os_1_count": 20,
"top_os_2_name": "Debian",
"top_os_2_count": 12,
"top_os_3_name": "Rocky Linux",
"top_os_3_count": 10,
"os_distribution": [...],
"last_updated": "2025-10-11T12:34:56.789Z"
}
Any of these numeric fields (including top_os_*_count) can be used in your GetHomepage mappings!
To find out what your top 3 operating systems are, look for the top_os_1_name, top_os_2_name, and top_os_3_name values in the response.
Pretty Print for Easy Reading
Use jq to format the output nicely:
curl -H "Authorization: Basic YOUR_BASE64_CREDENTIALS" \
http://your-patchmon-url:3000/api/v1/gethomepage/stats | jq
This makes it easier to see what metrics your instance provides.
How to Display Your OS Distribution
Step 1: Discover your top operating systems
Run the curl command and look for these fields:
curl -s -H "Authorization: Basic YOUR_BASE64_CREDENTIALS" \
http://your-patchmon-url:3000/api/v1/gethomepage/stats | jq '{top_os_1_name, top_os_1_count, top_os_2_name, top_os_2_count, top_os_3_name, top_os_3_count}'
Example output:
{
"top_os_1_name": "Ubuntu",
"top_os_1_count": 35,
"top_os_2_name": "Debian",
"top_os_2_count": 18,
"top_os_3_name": "Rocky Linux",
"top_os_3_count": 12
}
Step 2: Add to your GetHomepage widget
Use the count fields (top_os_*_count) and label them with your actual OS names:
mappings:
- field: total_hosts
label: Total Hosts
- field: top_os_1_count
label: Ubuntu # Use your actual OS from top_os_1_name
- field: top_os_2_count
label: Debian # Use your actual OS from top_os_2_name
- field: top_os_3_count
label: Rocky Linux # Use your actual OS from top_os_3_name
Step 3: Restart GetHomepage
docker restart gethomepage
# or
systemctl restart gethomepage
Your widget will now show your infrastructure OS breakdown! 🎉
Security Best Practices
- Use HTTPS: Always use HTTPS in production for encrypted communication
- IP Restrictions: Limit API key usage to GetHomepage's IP address
- Set Expiration: Use expiration dates for temporary access
- Regular Rotation: Rotate API keys periodically
- Monitor Usage: Check "Last Used" timestamps for suspicious activity
- Unique Keys: Create separate API keys for different GetHomepage instances
- Secure Storage: Store GetHomepage
services.ymlsecurely with proper permissions
Complete Working Examples
Copy-Paste Ready Configurations
These are complete, working configurations you can copy directly into your services.yml file. Just replace the placeholders with your actual values.
Simple Dashboard Widget (Default)
This is the default configuration generated by PatchMon:
- PatchMon:
href: https://patchmon.example.com
description: Patch Management
icon: https://patchmon.example.com/assets/favicon.svg
widget:
type: customapi
url: https://patchmon.example.com/api/v1/gethomepage/stats
headers:
Authorization: Basic dXNlcjpwYXNzd29yZA==
mappings:
- field: total_hosts
label: Total Hosts
- field: hosts_needing_updates
label: Needs Updates
- field: security_updates
label: Security Updates
Detailed Monitoring Widget (Custom)
This example shows how to display 4 metrics including recent activity:
- PatchMon Production:
href: https://patchmon.example.com
description: Production Environment Patches
icon: https://patchmon.example.com/assets/logo_dark.png
widget:
type: customapi
url: https://patchmon.example.com/api/v1/gethomepage/stats
headers:
Authorization: Basic dXNlcjpwYXNzd29yZA==
mappings:
- field: total_hosts
label: Total Servers
- field: hosts_needing_updates
label: Needs Patching
- field: security_updates
label: Security Patches
- field: recent_updates_24h
label: Patched Today
Multiple Environments (Custom)
This example shows different metrics for different environments:
# Production - Focus on security
- PatchMon Prod:
href: https://patchmon-prod.example.com
description: Production Patches
icon: https://patchmon-prod.example.com/assets/favicon.svg
widget:
type: customapi
url: https://patchmon-prod.example.com/api/v1/gethomepage/stats
headers:
Authorization: Basic <prod_credentials>
mappings:
- field: total_hosts
label: Hosts
- field: security_updates
label: Security
- field: hosts_needing_updates
label: Pending
# Development - Focus on package count
- PatchMon Dev:
href: https://patchmon-dev.example.com
description: Development Patches
icon: https://patchmon-dev.example.com/assets/favicon.svg
widget:
type: customapi
url: https://patchmon-dev.example.com/api/v1/gethomepage/stats
headers:
Authorization: Basic <dev_credentials>
mappings:
- field: total_hosts
label: Hosts
- field: total_outdated_packages
label: Packages
- field: up_to_date_hosts
label: Updated
Maximum Information Widget (All 8 Metrics)
This example shows ALL available metrics (may be crowded):
- PatchMon Complete:
href: https://patchmon.example.com
description: Complete Statistics
icon: https://patchmon.example.com/assets/favicon.svg
widget:
type: customapi
url: https://patchmon.example.com/api/v1/gethomepage/stats
headers:
Authorization: Basic <credentials>
mappings:
- field: total_hosts
label: Total Hosts
- field: hosts_needing_updates
label: Needs Updates
- field: up_to_date_hosts
label: Up-to-Date
- field: security_updates
label: Security Updates
- field: hosts_with_security_updates
label: Security Hosts
- field: total_outdated_packages
label: Outdated Packages
- field: total_repos
label: Repositories
- field: recent_updates_24h
label: Updated (24h)
Note: Displaying all 8 metrics may make the widget tall. Choose 3-5 metrics that are most relevant to your needs.
Integration Architecture
┌─────────────────┐
│ GetHomepage │
│ Dashboard │
└────────┬────────┘
│
│ HTTP(S) Request
│ Authorization: Basic <base64>
│
▼
┌─────────────────┐
│ PatchMon │
│ API Server │
│ │
│ /api/v1/ │
│ gethomepage/ │
│ stats │
└────────┬────────┘
│
│ Query Database
│
▼
┌─────────────────┐
│ PostgreSQL │
│ Database │
│ │
│ - Hosts │
│ - Packages │
│ - Updates │
│ - Repositories │
└─────────────────┘
Rate Limiting
The GetHomepage integration endpoints are subject to PatchMon's general API rate limiting:
- Default: 100 requests per 15 minutes per IP address
- GetHomepage typically polls every 60 seconds
- This allows for normal operation without hitting limits
Support and Resources
- PatchMon Documentation: https://docs.patchmon.net
- GetHomepage Documentation: https://gethomepage.dev
- PatchMon Discord: https://patchmon.net/discord
- GitHub Issues: https://github.com/9technologygroup/patchmon.net/issues
Changelog
Version 1.0.1 (2025-10-11)
- Added OS distribution support
- New fields:
top_os_1_count,top_os_2_count,top_os_3_countfor displaying infrastructure OS breakdown - New fields:
top_os_1_name,top_os_2_name,top_os_3_namefor identifying operating systems - Total of 14 displayable metrics now available
Version 1.0.0 (2025-10-11)
- Initial GetHomepage integration release
- Basic authentication support
- Real-time statistics endpoint
- Customizable widget mappings
- IP restriction support
- API key management UI
- 8 core metrics available
Questions or issues? Join our Discord community or open a GitHub issue!
Setting up OIDC SSO Single Sign-on integration
Overview
PatchMon supports OpenID Connect (OIDC) authentication, allowing users to log in via an external Identity Provider (IdP) instead of, or in addition to, local username/password credentials.
Supported Providers
Any OIDC-compliant provider works, including:
- Authentik
- Keycloak
- Okta
- Azure AD (Entra ID)
- Google Workspace
- And others
What You Get
- SSO login via a configurable button on the login page
- Automatic user provisioning on first login (no need to create accounts manually)
- Group-based role mapping so your IdP controls who is an admin, user, or readonly viewer
- Optional - disable local password login entirely and enforce SSO for all users
Prerequisites
- PatchMon already installed and running
- An OIDC-compatible Identity Provider with an OAuth2/OIDC application configured
- HTTPS in production (OIDC routes enforce HTTPS when
NODE_ENV=production)
Step 1 - Create an OIDC Application in Your IdP
Create a new OAuth2 / OIDC application in your Identity Provider with the following settings:
| Setting | Value |
|---|---|
| Application type | Web application / Confidential client |
| Redirect URI | https://patchmon.example.com/api/v1/auth/oidc/callback |
| Scopes | openid, email, profile, groups |
| Grant type | Authorization Code |
| Token endpoint auth | Client Secret (Basic) |
After creating the application, note the Client ID and Client Secret as you'll need both.
Tip: If you plan to use group-based role mapping, ensure your IdP includes the
groupsclaim in the ID token. In Authentik, this is enabled by default. In Keycloak, you may need to add a "Group Membership" mapper to the client scope.
Provider-Specific Notes
Authentik:
- Create an OAuth2/OIDC Provider, then create an Application linked to it
- Issuer URL format:
https://auth.example.com/application/o/patchmon/ - Groups are included via the
groupsorak_groupsclaim (both are supported)
Keycloak:
- Create a Client with Access Type
confidential - Issuer URL format:
https://keycloak.example.com/realms/your-realm - Add a "Group Membership" protocol mapper to include groups in the token
Okta / Azure AD:
- Create an OIDC Web Application
- Ensure groups are included in the ID token claims
Step 2 - Configure PatchMon
Add the following environment variables to your .env file (for Docker deployments) or your backend environment.
Required Variables
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://auth.example.com/application/o/patchmon/
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
| Variable | Description |
|---|---|
OIDC_ENABLED |
Set to true to enable OIDC |
OIDC_ISSUER_URL |
Your IdP's issuer / discovery URL |
OIDC_CLIENT_ID |
Client ID from your IdP application |
OIDC_CLIENT_SECRET |
Client secret from your IdP application |
OIDC_REDIRECT_URI |
Must match exactly what you configured in your IdP |
Optional Variables
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_DISABLE_LOCAL_AUTH=false
OIDC_BUTTON_TEXT=Login with SSO
| Variable | Default | Description |
|---|---|---|
OIDC_SCOPES |
openid email profile groups |
Space-separated scopes to request. Include groups for role mapping |
OIDC_AUTO_CREATE_USERS |
true |
Automatically create a PatchMon account on first OIDC login |
OIDC_DEFAULT_ROLE |
user |
Role assigned when a user doesn't match any group mapping |
OIDC_DISABLE_LOCAL_AUTH |
false |
When true, hides the username/password fields and only shows the SSO button |
OIDC_BUTTON_TEXT |
Login with SSO |
Label shown on the SSO login button |
Step 3 - Group-Based Role Mapping (Optional)
Map your IdP groups to PatchMon roles so that role assignments stay in sync with your directory. Group matching is case-insensitive.
Role Hierarchy
PatchMon checks group membership in this order (highest priority first):
| PatchMon Role | Required IdP Group(s) | Description |
|---|---|---|
| Super Admin | Member of BOTH OIDC_ADMIN_GROUP AND OIDC_SUPERADMIN_GROUP |
Full access including system settings |
| Admin | Member of OIDC_ADMIN_GROUP |
Full access |
| Host Manager | Member of OIDC_HOST_MANAGER_GROUP |
Manage hosts and groups |
| User | Member of OIDC_USER_GROUP |
Standard access with data export |
| Readonly | Member of OIDC_READONLY_GROUP |
View-only access |
| Default | None of the above | Gets OIDC_DEFAULT_ROLE (defaults to user) |
Environment Variables
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SUPERADMIN_GROUP=PatchMon SuperAdmins
OIDC_HOST_MANAGER_GROUP=PatchMon Host Managers
OIDC_READONLY_GROUP=PatchMon Readonly
OIDC_SYNC_ROLES=true
| Variable | Description |
|---|---|
OIDC_ADMIN_GROUP |
IdP group name that maps to Admin role |
OIDC_USER_GROUP |
IdP group name that maps to User role |
OIDC_SUPERADMIN_GROUP |
IdP group name that maps to Super Admin (requires both this and Admin group) |
OIDC_HOST_MANAGER_GROUP |
IdP group name that maps to Host Manager role |
OIDC_READONLY_GROUP |
IdP group name that maps to Readonly role |
OIDC_SYNC_ROLES |
When true (default), the user's role is updated on every login based on current group membership. When false, the role is only set on first login |
You only need to define the groups you intend to use. Any variables left unset are simply ignored.
Step 4 - Restart PatchMon
After updating your .env file, restart the backend so it discovers your OIDC provider on startup:
# Docker
docker compose restart backend
# Or if rebuilding
docker compose up -d --force-recreate backend
Check the backend logs to confirm OIDC initialised:
docker compose logs backend | grep -i oidc
You should see:
Discovering OIDC configuration from: https://auth.example.com/...
OIDC Issuer discovered: https://auth.example.com/...
OIDC client initialized successfully
If you see OIDC is enabled but missing required configuration, double-check your environment variables.
Step 5 - Test the Login
- Open PatchMon in your browser
- You should see a "Login with SSO" button (or your custom
OIDC_BUTTON_TEXT) - Click it and you'll be redirected to your IdP
- Authenticate with your IdP credentials
- You'll be redirected back to PatchMon and logged in
If OIDC_AUTO_CREATE_USERS is true, a PatchMon account is created automatically using your email address. The username is derived from the email prefix (e.g. john.doe@example.com becomes john.doe).
First-Time Setup (No Users Exist Yet)
When PatchMon has no users in the database, it displays a setup wizard. If you're using OIDC-only mode (OIDC_DISABLE_LOCAL_AUTH=true), you have two options:
Option A - Log In via OIDC (Recommended)
- Ensure your IdP user is in the admin group (e.g.
PatchMon Admins) - Set
OIDC_AUTO_CREATE_USERS=true - Click the SSO button and the first user will be created with the role determined by your group mapping
Option B - Disable OIDC for the first Admin
If the setup wizard blocks access then you can create a local Admin on first setup then enable/setup OIDC after that. You can remove the first admin user but you should be Super Admin Role.
What Syncs from Your IdP
On every OIDC login, PatchMon automatically syncs the following from your Identity Provider:
- Role (if
OIDC_SYNC_ROLES=true) - based on group membership - Avatar / profile picture - if the
pictureclaim is present - First name and last name - from
given_nameandfamily_nameclaims - Email - used for matching and account linking
Account Linking
If a local PatchMon user already exists with the same email as the OIDC user, PatchMon will automatically link the accounts, but only if the email is marked as verified by the IdP. This prevents account takeover via unverified emails.
Disabling Local Authentication
To enforce SSO for all users, set:
OIDC_DISABLE_LOCAL_AUTH=true
This hides the username/password fields on the login page and only shows the SSO button. Local authentication is only actually disabled if OIDC is also enabled and successfully initialised. This safety check prevents you from being locked out if OIDC is misconfigured.
Important: Ensure at least one OIDC user has admin access before enabling this, or you may lose the ability to manage PatchMon.
Complete Example Configuration
Authentik
# .env
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example.com/application/o/patchmon/
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Authentik
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true
Keycloak
# .env
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://keycloak.example.com/realms/your-realm
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Keycloak
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true
Troubleshooting
OIDC Not Initialising
Logs show: OIDC is enabled but missing required configuration
All four required variables must be set: OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI. Check for typos or empty values.
SSO Button Not Appearing
- PatchMon cannot reach the IdP (DNS / firewall issue)
- Issuer URL is incorrect
- IdP's
.well-known/openid-configurationendpoint is not accessible
"Authentication Failed" After Redirect
- Verify the Redirect URI in your IdP matches
OIDC_REDIRECT_URIexactly (including trailing slashes) - Ensure cookies are not being blocked (OIDC uses httpOnly cookies for session state)
- Check that your IdP supports PKCE (PatchMon uses S256 code challenge)
"Session Expired" Error
The OIDC session has a 10-minute window between initiating login and completing the callback. If the user takes too long at the IdP, the session expires. Simply try logging in again.
User Gets Wrong Role
- Check that the
groupsscope is included inOIDC_SCOPES - Verify your IdP is including groups in the ID token (not just the access token)
- Check backend logs as they show which groups were received:
OIDC groups found: [...] - If logs show
No groups found in OIDC token, configure your IdP to include the groups claim - Group matching is case-insensitive, so
patchmon adminsmatchesPatchMon Admins
"User Not Found" Error
OIDC_AUTO_CREATE_USERS is set to false and no matching PatchMon account exists. Either enable auto-creation or create the user account manually in PatchMon first (the email must match).
Debug Logging
For detailed OIDC troubleshooting, enable debug logging:
LOG_LEVEL=debug
Then check the backend logs:
docker compose logs -f backend | grep -i oidc
Security Notes
- HTTPS is enforced for OIDC login and callback routes when
NODE_ENV=production - PKCE (S256) is used for all authorization code exchanges
- Tokens are stored in httpOnly cookies, not localStorage, to prevent XSS attacks
- Client secrets should never be committed to version control
- Account linking only occurs when the IdP reports the email as verified
- Role sync can be disabled (
OIDC_SYNC_ROLES=false) if you prefer to manage roles manually in PatchMon after first login
Release Notes Docs
1.4.1
🎉 PatchMon 1.4.1
A maintenance release with OIDC improvements, FreeBSD agent support, installer fixes, and various bug fixes and improvements.
🔐 OIDC Improvements and Hot Fixes
- OIDC authentication fixes and stability improvements
- Hot fixes for edge cases in SSO flows
🖥️ FreeBSD Agent Support
- Native FreeBSD agent support — run the PatchMon agent on FreeBSD hosts
- Initial FreeBSD support via community contribution
📦 Native Installer Upgrade Fixes
- Fixes for native installer upgrade paths
- Improved reliability when upgrading existing installations
🐛 Host Table Views Not Saving -> Bug Fix
- Fixed an issue where host table view preferences (columns, sort order, filters) were not being saved
- Table view state now persists correctly across sessions
🔧 Agent Memory Leaks and Improvements
- Addressed memory leaks in the agent
- General agent stability and resource usage improvements
🔒 Better API Integration Scoping
- Improved scoping for Integration API credentials and access
- Tighter integration between API keys and their permitted scope
🙏 Acknowledgements
- @RuTHlessBEat200 — for agent and OIDC fixes
- @mminkus — for FreeBSD initial PR
- The rest of the community for their support and help on Discord and GitHub
1.4.0 (Major)
🎉 PatchMon 1.4.0
A major release with security compliance scanning, OIDC SSO, an alerting engine, web SSH terminal, and AI-assisted terminal support.
🛡️ Security Compliance Scanning
- OpenSCAP CIS Benchmark scanning directly from the agent (Level 1 / Level 2)
- Docker Bench for Security when Docker integration is enabled
- Compliance dashboard with fleet-wide scores, pass/fail breakdowns, and scan history
- Optional auto-remediation of failed rules during scans
🔐 OIDC Single Sign-On
- OpenID Connect authentication with Authentik, Keycloak, Okta, or any OIDC provider
- Automatic user provisioning on first OIDC login
- Group-based role mapping from your identity provider to PatchMon roles
- Option to disable local auth and enforce SSO-only login
🔔 Alerting & Reporting
- New Reporting page with filtering by severity, type, status, and assignment
- Host Down alerts real time view of host uptime
- Alert types including server update, agent update, and host down
- Per-alert-type configuration for default severity, auto-assignment, escalation, and retention
💻 Web SSH Terminal
- Browser-based SSH to any host from the PatchMon UI
- Direct and proxy modes (proxy mode routes through the agent, no SSH port exposure needed)
🤖 AI Terminal Assistant
- AI chat panel inside the SSH terminal for command suggestions and troubleshooting
- Multiple providers supported: OpenRouter, Anthropic, OpenAI, Google Gemini
- Context-aware using your recent terminal output
🖥️ UI Improvements
- Toast notifications replacing disruptive
alert()popups - Error boundary with crash recovery and a copyable error report
- "Waiting for Connection" screen with real-time status when onboarding a new host
- Swagger / OpenAPI docs served at
/api-docson the server
🔧 Other
- Superuser management permission (
can_manage_superusers) for finer-grained RBAC - More stats and details on hosts with added flags such as
?include=statsor?updates_only=true
Plus Much Much More
1.3.7
📝 ALERT : Auto-update of Agent issue
Versions <1.3.6 have an issue where the service does not restart after auto-update. OpenRC systems are unaffected and work correctly.
This means you will unfortunately have to use systemctl start patchmon-agent on your systems to load up 1.3.7 agent when it auto-updates shortly.
Very sorry for this, future versions are fixed - I built this release notes notification feature specifically to notify you of this.
🎉 New Features & Improvements :
Mobile UI: Mobile user interface improvements are mostly complete, providing a better experience on mobile devices.
Systemctl Helper Script: In future versions (1.3.7+), a systemctl helper script will be available to assist with auto-update service restarts.
Staggered Agent Intervals: Agents now report at staggered times to prevent overwhelming the PatchMon server. If the agent report interval is set to 60 minutes, different hosts will report at different times. This is in the config.yml as "report_offset: xxxx" in seconds
Reboot Detection Information: Reboot detection information is now stored in the database. When the "Reboot Required" flag is displayed, hovering over it will show the specific reason why a reboot is needed (Reboot feature still needs work and it will be much better in 1.3.8)
JSON Report Output: The patchmon-agent report --json command now outputs the complete report payload to the console in JSON format instead of sending it to the PatchMon server. This is very useful for integrating PatchMon agent data with other tools and for diagnostic purposes.
Persistent Docker Toggle: Docker integration toggle state is now persisted in the database, eliminating in-memory configuration issues. No more losing Docker settings on container restarts (thanks to the community for initiating this feature).
Config.yml Synchronization: The agent now writes and compares the config.yml file with the server configuration upon startup, ensuring better synchronization of settings between the agent and server.
Network Information Page: Enhanced network information page to display IPv6 addresses and support multiple network interfaces, providing more comprehensive network details.
Auto-Update Logic Fix: Fixed an issue where agents would auto-update even when per-host auto-update was disabled. The logic now properly honours both server-wide auto-update settings and per-host auto-update settings.
Prisma Version Fix: Fixed Prisma version issues affecting Kubernetes deployments by statically setting the Prisma version in package.json files.
Hiding Github Version: Added a toggle in Server Version settings to disable showing the github release notes on the login screen
Thank you to all contributors :D
1.3.6
Fixed ProxMox Auto-enrollment script
1.3.5
- Fixed critical Bug relating to auto-update failing and looping in a reboot due to incorrect version checking mechanism.
Especially on x86 or ARM processors, the version checking method was flawed so it kept trying to reinstall the agent.
This release will be further elaborated on but for now marking as latest.
1.3.4
✨Fixes and Enhancements
Alpine Support
Version 1.3.4 brings about better apk support for Alpine OS
Auto-enrollment API
In Integration settings you can now create a single command (like a master command) which does not require that you add the host first. This is useful for embedding inside ansible deployment scripts or other use-cases where you have quite a few hosts to add.
NOTE: Proxmox api endpoint has changed:
It now goes like this:
curl -s "https://patchmon-url/api/v1/auto-enrollment/script?type=proxmox-lxc&token_key=KEY&token_secret=SECRET" | bash
Notice that at the end of the auto-enrollment we have a new endpoint called script , which then specifies the script type such as proxmox-lxc
Uninstallation command updated and script to remove the instance totally (with the ability to optionally remove backups of agents etc)
Reboot Needed flag
The server now gives a tag and notification if a host needs rebooting due to the kernel version mismatching when installed kernel differs from the running kernel. There is also a new dashboard card that shows this qty in the hosts table.
Other improvements
- Now uses POSIX compatible installation scripts
- Does not use /bin/bash , now we use /bin/sh
- Added robots.txt to discourage search engines from discovering a public facing version of PatchMon
Upgrading note / instructions
Some members are reporting a upgrade Loop on their systems, please stop the patchmon-agent and start it again
systemctl stop patchmon-agent && systemctl start patchmon-agent after the upgrade.
The issue is that the built-in restart function after downloading the binary isn't loading the new binary files so it's using what's loading in cache/memory.
In the newer versions we have introduced a helper-script
Upgrading
Docker
Pull the latest image and bring it up, nothing new needs doing to env or container settings.
Bare metal
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh --update
ProxMox community Script
Go into the LXC and type in update
https://community-scripts.github.io/ProxmoxVE/scripts?id=patchmon
Agents
Agents will auto-upgrade to 1.3.4 if the settings have been selected to allow this. Pinned release for the agent repo : https://github.com/PatchMon/PatchMon-agent/releases/tag/1.3.4
Many thanks to the community for their hard work and support. <3
https://buymeacoffee.com/iby___
1.3.3
✨Fixes and Enhancements
ARM support
Supports the installation of ARM and ARM64 agents. Drop down added when creating the command for the installation of the agent and also modified the logic of version handling when the PatchMon server is hosted on an ARM based server. This is because previously the server was checking the current version of its binary but it was pinned to checking the amd64 version of the binary, now this is dynamic based on the actual architecture of the PatchMon server.
Disabling / Enabling docker integration
In the individual hosts page there is now an integrations tab which allows the user to enable or disable docker integration.
This amends the /etc/patchmon/config.yml with the relevant settings.
Dashboard Chart fix
Previously the data taken for this chart was taken from the hosts details data but this did not honor unique packages so the quantities was inflated. Now we have a separate database table model that collects information every 30 minutes for data metrics. This is much more efficient and the charts are now displaying accurate trends.
RHEL fixes
RHEL derived Operating systems such as AlmaLinux, Oracle Linux etc had a bug in the agent that was using the dnf package manager where the version data was not populated in the json payload causing errors upon sending the report. This has now been fixed and also security package quantities are also showing.
TimeZone support
The server environment file now supports a TIMEZONE= variable to show things in the right timezone on the app.
Backend container crashing
This was due to error handling not in place when there was docker events that were closed unexpectedly. This has been fixed to handle it correctly.
Ui fixes
Left justification on tables in the repos page Sorting by Security in repos page now fixed
Upgrading
Docker
Pull the latest image and bring it up, nothing new needs doing to env or container settings.
Bare metal
curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh --update
ProxMox community Script
Go into the LXC and type in update
https://community-scripts.github.io/ProxmoxVE/scripts?id=patchmon
Agents
Agents will auto-upgrade to 1.3.3 if the settings have been selected to allow this. Pinned release for the agent repo : https://github.com/PatchMon/PatchMon-agent/releases/tag/1.3.3
Many thanks to the community for their hard work and support. <3
https://buymeacoffee.com/iby___
1.3.2
✨ Major Features
Docker Support (still in beta)
Previously the docker collector was a script that was ran (also through cron), now it's baked into the Agent binary file and therefore no need for a separate bash script. It also leverages the same credentials.yml which was introduced in 1.3.0. We have also added more information that is collected such as networks and volumes for a complete picture of your Docker environment.
Forced agent update button
You'll now find a button on the host page to force update the agent if it doesn't wish to update automatically.
UI themes to chose from
A few new branding Ui themes have been added allowing you to chose what theme to apply to PatchMons interface. This is currently app-wide and it requires that dark-mode is enabled for these themes to work.
Performance
Additional environment variables have been added relating to Database connections, these are all documented here : Environment Documentation
Metrics
We have introduced a metrics system, more information is found here about how our metrics collection works, what data is collected and opting in/out etc : https://docs.patchmon.net/books/patchmon-application-documentation/page/metrics-collection-information
TFA / Backup Codes
Fixed TFA remember me not actually remembering Fixed Backup Codes entering, they can now be used in the same text box as the code itself
Fixes
- Fixed Host timeout issue due to SSE connection issues on frontend
- Fixed https go agent communication with server
- Fixed Docker inventory collection
- Fixed TFA and Backup Codes
- Fixed not grouping by groups in the hosts table
- IPv6 listening support added in Nginx config by community member @alan7000
- When Deleting Groups it shows the hosts that are being affected
P.S I skipped 1.3.1 version tag because some members in the community have 1.3.1 when I was building it, if we release it as 1.3.1 then their agents won't really update properly - catering for the few.
Docker upgrade instructions video : https://www.youtube.com/watch?v=bi_r9aW2uQA Written Instructions in docs : https://docs.patchmon.net/books/patchmon-application-documentation/page/upgrading-from-130-to-132
For bare-metal type the curl -fsSL -o setup.sh https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh --update should update your instance
Many thanks to the community for their hard work and support. <3 iby___
1.3.0 (Major)
🚀 PatchMon version 1.3.0
This major release brings a new and improved architecture on the server and agent.
✨ Major Features
GO based Agent
Agent is now a GO based Agent binary file that runs as a service systemctl status patchmon-agent
The Agent serves a websocket connection to PatchMon server.
Agent has been compiled in amd64, i386, arm and arm64
A new repository has been setup for the agent.
BullMQ + Redis
The PatchMon Server runs a BullMQ service which utilises Redis server for scheduling automated and queued jobs. Jobs include things like "Cleanup orphaned repos" where it will remove repositories that are now not associated with any hosts etc Bullboard has also been added so that we can have a dashboard to monitor the jobs from a server level.
WebSocket
PatchMon Agents now connect via Web Socket Secure (https) or Web Socket (ws) to listen for commands from PatchMon. The Agents themselves control the schedule of reporting information however this persistent and bi-directional connection architecture lays the foundation of PatchMon so that it can control and handle management etc.
Performance
Various performance related improvements have been made with the way that node.js uses prisma for the Postgresql ORM. There was a lot of connection leakage where instead of utilising established connections it would create a new connection to the Database. These were causing at times Database connections to rise above 100! Fixes also improved the way the front-end speaks to the /api/v1 endpoints. These remove and handle the 429 (rate limit errors) and other backend errors.
Security
Various security handling has been improved around cookie handling, cors handling etc so that /bullboard can be authenticated
Agent updates checking
New mechanism for checking for Go based agents. The PatchMon server will query the GitHub repo and allow you to download the agents directly whilst the agents themselves will query PatchMon. I have pinned the agent version with the server version and had the agents query the server for downloading the updates as opposed to downloading them from github. This is because I plan to use PGP for signing agents off in the future and improve security mechanisms for server/agent verification.
Upgrading
Bash scripts from 1.2.8 will use an intermediary script of 1.2.9 which will run the installation of the new agent service. Docker upgrade instructions video : https://www.youtube.com/watch?v=NZE2pi6WxWM Written Instructions in docs : https://docs.patchmon.net/books/patchmon-application-documentation/page/upgrading-from-128-to-130 Coming soon:
For bare-metal type the setup.sh update is being modified soon to handle the installation and setup or Redis 7 DB user and password as well as the nginx configuration amendments to handle upgrade on the websocket and add the /bullboard directive.
Many thanks to the community for their hard work and support. <3 iby___
1.2.8 to 1.3.0 - Upgrade
Upgrading the Server
Introduction
Upgrade Video link : https://www.youtube.com/watch?v=NZE2pi6WxWM
There are 3 main changes between version 1.2.X and 1.3.x:
- Go-based Agent Binary: The introduction of a binary based on Go for the agent, replacing the previous bash scripts. This binary executes much faster and is compatible across different architectures when compiled.
- Redis and BullMQ Integration: The introduction of Redis as a back-end database server and BullMQ as the queue manager for tasks and automation management.
- Nginx Configuration: The addition of an nginx block for the presentation of the /bullboard URL.
Let's go through the two types of upgrades:
Docker Upgrade
This is quite simple as we just need to add the following in the container configuration for Redis:
- Add the Redis service
- Add the Redis configuration in the backend environment
- Add a new redis_data volume
Important: Ensure you change the Redis password and update it in all three areas where "your-redis-password-here" is specified. This password should be secure but some alphanumeric characters can cause issues.
Docker Compose Ammendments
name: patchmon
services:
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass your-redis-password-here # CHANGE THIS TO YOUR REDIS PASSWORD
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "your-redis-password-here", "ping"] # CHANGE THIS TO YOUR REDIS PASSWORD
interval: 3s
timeout: 5s
retries: 7
backend:
environment:
# Redis Configuration
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: your-redis-password-here # CHANGE THIS TO YOUR REDIS PASSWORD
REDIS_DB: 0
# ... other environment variables
volumes:
redis_data:
Migration issues
If you get a migration issue like this:
backend-1 | Error: P3009
backend-1 |
backend-1 | migrate found failed migrations in the target database, new migrations will not be applied. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
backend-1 | The 20251005000000_add_user_sessions migration started at 2025-10-21 22:50:32.244874 UTC failed
backend-1 |
backend-1 |
dependency failed to start: container patchmon-backend-1 is unhealthy
Then you need to apply the following commands from the directory where the docker-compose.yml file is:
Depending on your docker environment and version it may be as docker compose run
docker-compose run --rm backend npx prisma migrate resolve --rolled-back 20251005000000_add_user_sessions
docker-compose run --rm backend npx prisma migrate resolve --applied 20251005000000_add_user_sessions
Bare Metal / VM Upgrade
Instructions for bare metal and VM upgrades will be detailed in the following sections... soon... Still building the script to handle the update ...
Agent Side Management
patchmon-agent Management
Overview
The PatchMon agent is a compiled Go binary (patchmon-agent) that runs as a persistent service on monitored hosts. It maintains a WebSocket connection to the PatchMon server for real-time communication, sends periodic package and system reports, collects integration data (Docker, compliance), and supports remote commands such as SSH proxy sessions.
This guide covers everything you need to manage the agent after installation: CLI commands, service management, log access, troubleshooting, updates, and removal.
Key Facts
| Property | Value |
|---|---|
| Binary location | /usr/local/bin/patchmon-agent |
| Configuration directory | /etc/patchmon/ |
| Config file | /etc/patchmon/config.yml |
| Credentials file | /etc/patchmon/credentials.yml |
| Log file | /etc/patchmon/logs/patchmon-agent.log |
| Service name | patchmon-agent (systemd or OpenRC) |
| Runs as | root |
| Primary mode | patchmon-agent serve (long-lived service) |
Table of Contents
- CLI Command Reference
- Service Management
- Viewing Logs
- Testing and Diagnostics
- Manual Reporting
- Configuration Management
- Agent Updates
- Agent Removal
- Common Troubleshooting
- Architecture and Supported Platforms
CLI Command Reference
All commands must be run as root (or with sudo). The agent will refuse to run if it does not have root privileges.
Quick Reference
patchmon-agent [command] [flags]
| Command | Description | Requires Root |
|---|---|---|
serve |
Run the agent as a long-lived service (primary mode) | Yes |
report |
Collect and send a one-off system/package report | Yes |
report --json |
Output the report payload as JSON to stdout (does not send) | Yes |
ping |
Test connectivity and validate API credentials | Yes |
diagnostics |
Show comprehensive system and agent diagnostics | Yes |
config show |
Display current configuration and credential status | No |
config set-api |
Configure API credentials and server URL | Yes |
check-version |
Check if an agent update is available | Yes |
update-agent |
Download and install the latest agent version | Yes |
version |
Print the agent version | No |
Global Flags
These flags can be used with any command:
| Flag | Default | Description |
|---|---|---|
--config <path> |
/etc/patchmon/config.yml |
Path to the configuration file |
--log-level <level> |
info |
Override log level (debug, info, warn, error) |
--version |
— | Print the agent version and exit |
--help |
— | Show help for any command |
serve — Run as a Service
sudo patchmon-agent serve
This is the primary operating mode. It is what the systemd/OpenRC service unit executes. When started, it:
- Loads configuration and credentials from
/etc/patchmon/ - Sends a startup ping to the PatchMon server
- Establishes a persistent WebSocket connection (real-time commands)
- Sends an initial system report in the background
- Starts periodic reporting on the configured interval (default: 60 minutes)
- Syncs integration status and update interval from the server
- Listens for server-initiated commands (report now, update, compliance scan, etc.)
You should not normally run serve manually — it is managed by the system service. If you need to test it interactively, stop the service first to avoid duplicate instances.
Example — running interactively for debugging:
# Stop the service first
sudo systemctl stop patchmon-agent
# Run with debug logging to see all output
sudo patchmon-agent serve --log-level debug
# When finished, restart the service
sudo systemctl start patchmon-agent
report — Send a One-Off Report
sudo patchmon-agent report
Collects system information, installed packages, repository data, hardware info, network details, and integration data (Docker containers, compliance scans), then sends everything to the PatchMon server.
After sending the report, the agent also:
- Checks for available agent updates and applies them if auto-update is enabled
- Collects and sends integration data (Docker, compliance) separately
Output:
The command logs its progress to the configured log file. To see output directly, run with --log-level debug or check the log file.
report --json — Output Report as JSON
sudo patchmon-agent report --json
Collects the same system and package data but outputs the full JSON payload to stdout instead of sending it to the server. This is extremely useful for:
- Debugging — see exactly what data the agent would send
- Validation — verify package detection is correct
- Integration — pipe JSON to other tools for analysis
Example — inspect the report payload:
sudo patchmon-agent report --json | jq .
Example — check which packages need updates:
sudo patchmon-agent report --json | jq '[.packages[] | select(.needsUpdate == true)] | length'
Example — save a snapshot for later comparison:
sudo patchmon-agent report --json > /tmp/patchmon-report-$(date +%Y%m%d).json
Note: The
--jsonflag does not send data to the server and does not require valid API credentials. It only requires root access to read system package information.
ping — Test Connectivity
sudo patchmon-agent ping
Tests two things:
- Network connectivity — can the agent reach the PatchMon server?
- API credentials — are the
api_idandapi_keyvalid?
Success output:
✅ API credentials are valid
✅ Connectivity test successful
Failure output example:
Error: connectivity test failed: server returned 401
Use this command immediately after installation or whenever you suspect credential or network issues.
diagnostics — Full System Diagnostics
sudo patchmon-agent diagnostics
Displays a comprehensive diagnostic report covering:
| Section | Details |
|---|---|
| System Information | OS type/version, architecture, kernel version, hostname, machine ID |
| Agent Information | Agent version, config file path, credentials file path, log file path, log level |
| Configuration Status | Whether config and credentials files exist (✅/❌) |
| Network Connectivity | Server URL, TCP reachability test, API credential validation |
| Recent Logs | Last 10 log entries from the agent log file |
Example output:
PatchMon Agent Diagnostics v1.4.0
System Information:
OS: ubuntu 22.04
Architecture: amd64
Kernel: 5.15.0-91-generic
Hostname: webserver-01
Machine ID: a1b2c3d4e5f6...
Agent Information:
Version: 1.4.0
Config File: /etc/patchmon/config.yml
Credentials File: /etc/patchmon/credentials.yml
Log File: /etc/patchmon/logs/patchmon-agent.log
Log Level: info
Configuration Status:
✅ Config file exists
✅ Credentials file exists
Network Connectivity & API Credentials:
Server URL: https://patchmon.example.com
✅ Server is reachable
✅ API is reachable and credentials are valid
Last 10 log entries:
2026-02-12T10:30:00 level=info msg="Report sent successfully"
...
This is the best single command for troubleshooting agent issues.
config show — View Current Configuration
sudo patchmon-agent config show
Displays the current configuration values and credential status:
Configuration:
Server: https://patchmon.example.com
Agent Version: 1.4.0
Config File: /etc/patchmon/config.yml
Credentials File: /etc/patchmon/credentials.yml
Log File: /etc/patchmon/logs/patchmon-agent.log
Log Level: info
Credentials:
API ID: patchmon_a1b2c3d4
API Key: Set ✅
Security: The API key is never shown. The output only confirms whether it is set.
config set-api — Configure Credentials
sudo patchmon-agent config set-api <API_ID> <API_KEY> <SERVER_URL>
Sets up the agent's API credentials and server URL. This command:
- Validates the inputs (non-empty, valid URL format)
- Saves the server URL to
/etc/patchmon/config.yml - Saves the credentials to
/etc/patchmon/credentials.yml(with600permissions) - Runs an automatic connectivity test (
ping)
Example:
sudo patchmon-agent config set-api \
patchmon_a1b2c3d4 \
abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 \
https://patchmon.example.com
Note: This command is primarily useful for manual installations or credential rotation. The standard install script sets credentials automatically.
check-version — Check for Updates
sudo patchmon-agent check-version
Queries the PatchMon server to see if a newer agent version is available.
Output when up to date:
Agent is up to date (version 1.4.0)
Output when update is available:
Agent update available!
Current version: 1.3.2
Latest version: 1.4.0
To update, run: patchmon-agent update-agent
Output when auto-update is disabled on the server:
Current version: 1.3.2
Latest version: 1.4.0
Status: Auto-update disabled by server administrator
To update manually, run: patchmon-agent update-agent
update-agent — Update to Latest Version
sudo patchmon-agent update-agent
Downloads the latest agent binary from the PatchMon server and performs an in-place update. The process:
- Checks for recent updates (prevents update loops within 5 minutes)
- Queries the server for the latest version
- Downloads the new binary
- Verifies binary integrity via SHA-256 hash comparison (mandatory)
- Creates a timestamped backup of the current binary (e.g.,
patchmon-agent.backup.20260212_143000) - Writes the new binary to a temporary file and validates it
- Atomically replaces the current binary
- Cleans up old backups (keeps the last 3)
- Restarts the service (systemd or OpenRC) via a helper script
Security features:
- Binary hash verification is mandatory — the agent refuses to update if the server does not provide a hash
- Hash mismatch (possible tampering) blocks the update
skip_ssl_verifyis blocked in production environments for binary downloads- Backup files use
0700permissions (owner-only)
Note: In normal operation, the agent auto-updates when the server signals a new version. You only need to run
update-agentmanually when auto-update is disabled or if you want to force an immediate update.
version — Print Version
patchmon-agent version
# or
patchmon-agent --version
Prints the agent version:
PatchMon Agent v1.4.0
This does not require root access.
Service Management
The PatchMon agent runs as a system service managed by systemd (most Linux distributions) or OpenRC (Alpine Linux). In environments where neither is available, a crontab fallback is used.
Systemd (Ubuntu, Debian, CentOS, RHEL, Rocky, Alma, Fedora, etc.)
Service File Location
/etc/systemd/system/patchmon-agent.service
Service File Contents
The installer creates this unit file automatically:
[Unit]
Description=PatchMon Agent Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/patchmon-agent serve
Restart=always
RestartSec=10
WorkingDirectory=/etc/patchmon
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=patchmon-agent
[Install]
WantedBy=multi-user.target
Key properties:
Restart=always— the service automatically restarts if it crashes or is killedRestartSec=10— waits 10 seconds before restarting (prevents rapid restart loops)After=network.target— ensures the network is up before starting- Logs go to the systemd journal as well as the agent's own log file
Common systemd Commands
# Check if the agent is running
sudo systemctl status patchmon-agent
# Start the agent
sudo systemctl start patchmon-agent
# Stop the agent
sudo systemctl stop patchmon-agent
# Restart the agent (e.g., after config changes)
sudo systemctl restart patchmon-agent
# Enable auto-start on boot
sudo systemctl enable patchmon-agent
# Disable auto-start on boot
sudo systemctl disable patchmon-agent
# Check if enabled
sudo systemctl is-enabled patchmon-agent
# Check if active
sudo systemctl is-active patchmon-agent
# Reload systemd after editing the service file manually
sudo systemctl daemon-reload
Reading systemd Journal Logs
# Follow logs in real-time (like tail -f)
sudo journalctl -u patchmon-agent -f
# Show last 50 log entries
sudo journalctl -u patchmon-agent -n 50
# Show logs since last boot
sudo journalctl -u patchmon-agent -b
# Show logs from the last hour
sudo journalctl -u patchmon-agent --since "1 hour ago"
# Show logs from a specific date
sudo journalctl -u patchmon-agent --since "2026-02-12 10:00:00"
# Show only errors
sudo journalctl -u patchmon-agent -p err
# Show logs without pager (useful for scripts)
sudo journalctl -u patchmon-agent --no-pager -n 100
# Export logs to a file
sudo journalctl -u patchmon-agent --no-pager > /tmp/patchmon-logs.txt
OpenRC (Alpine Linux)
Service File Location
/etc/init.d/patchmon-agent
Service File Contents
#!/sbin/openrc-run
name="patchmon-agent"
description="PatchMon Agent Service"
command="/usr/local/bin/patchmon-agent"
command_args="serve"
command_user="root"
pidfile="/var/run/patchmon-agent.pid"
command_background="yes"
working_dir="/etc/patchmon"
depend() {
need net
after net
}
Common OpenRC Commands
# Check if the agent is running
sudo rc-service patchmon-agent status
# Start the agent
sudo rc-service patchmon-agent start
# Stop the agent
sudo rc-service patchmon-agent stop
# Restart the agent
sudo rc-service patchmon-agent restart
# Add to default runlevel (auto-start on boot)
sudo rc-update add patchmon-agent default
# Remove from default runlevel
sudo rc-update del patchmon-agent default
# List services in default runlevel
sudo rc-update show default
Reading Logs on Alpine/OpenRC
OpenRC does not have a journal. Logs are written only to the agent's log file:
# Follow logs in real-time
sudo tail -f /etc/patchmon/logs/patchmon-agent.log
# Show last 50 lines
sudo tail -n 50 /etc/patchmon/logs/patchmon-agent.log
# Search logs for errors
sudo grep -i "error\|fail" /etc/patchmon/logs/patchmon-agent.log
Crontab Fallback (No Init System)
In minimal containers or environments without systemd or OpenRC, the installer sets up a crontab entry:
@reboot /usr/local/bin/patchmon-agent serve >/dev/null 2>&1
The agent is also started immediately in the background during installation.
Managing the Crontab Fallback
# Check for PatchMon crontab entries
crontab -l | grep patchmon
# Stop the agent manually
sudo pkill -f 'patchmon-agent serve'
# Start the agent manually
sudo /usr/local/bin/patchmon-agent serve &
# Restart the agent
sudo pkill -f 'patchmon-agent serve' && sudo /usr/local/bin/patchmon-agent serve &
Viewing Logs
The agent writes logs to two locations depending on the init system:
| Init System | Journal | Log File |
|---|---|---|
| systemd | ✅ journalctl -u patchmon-agent |
✅ /etc/patchmon/logs/patchmon-agent.log |
| OpenRC | ❌ | ✅ /etc/patchmon/logs/patchmon-agent.log |
| Crontab | ❌ | ✅ /etc/patchmon/logs/patchmon-agent.log |
Log File Details
| Property | Value |
|---|---|
| Location | /etc/patchmon/logs/patchmon-agent.log |
| Max size | 10 MB per file |
| Max backups | 5 rotated files |
| Max age | 14 days |
| Compression | Yes (old logs compressed automatically) |
| Rotation | Automatic (handled by the agent, not logrotate) |
The agent uses the lumberjack library for built-in log rotation. You do not need to configure logrotate separately.
Log Levels
Set the log level in /etc/patchmon/config.yml or via the --log-level flag:
| Level | Description | Use Case |
|---|---|---|
debug |
Verbose — every operation, request/response bodies, package details | Active troubleshooting |
info |
Normal — key events, report summaries, connectivity status | Default / production |
warn |
Warnings — non-critical failures, retries, degraded operation | Noise reduction |
error |
Errors only — critical failures that need attention | Minimal logging |
Change log level temporarily (until service restart):
sudo patchmon-agent report --log-level debug
Change log level permanently:
Edit /etc/patchmon/config.yml:
log_level: "debug"
Then restart the service:
sudo systemctl restart patchmon-agent
# or
sudo rc-service patchmon-agent restart
Log Format
Logs use structured text format with timestamps:
2026-02-12T10:30:00 level=info msg="Detecting operating system..."
2026-02-12T10:30:00 level=info msg="Detected OS" osType=ubuntu osVersion=22.04
2026-02-12T10:30:01 level=info msg="Found packages" count=247
2026-02-12T10:30:02 level=info msg="Sending report to PatchMon server..."
2026-02-12T10:30:03 level=info msg="Report sent successfully"
2026-02-12T10:30:03 level=info msg="Processed packages" count=247
2026-02-12T10:30:08 level=info msg="Agent is up to date" version=1.4.0
Testing and Diagnostics
Quick Health Check
Run these commands in order to verify the agent is working correctly:
# 1. Is the service running?
sudo systemctl status patchmon-agent # systemd
# or
sudo rc-service patchmon-agent status # OpenRC
# 2. Can the agent reach the server?
sudo patchmon-agent ping
# 3. Full diagnostics
sudo patchmon-agent diagnostics
# 4. What data would the agent send?
sudo patchmon-agent report --json | jq '.hostname, .os_type, .os_version, .packages | length'
Debugging a Problem
If the agent is not reporting data or appears offline:
# Step 1: Check service status
sudo systemctl status patchmon-agent
# Step 2: Check recent logs for errors
sudo journalctl -u patchmon-agent -n 30 --no-pager
# or
sudo tail -n 30 /etc/patchmon/logs/patchmon-agent.log
# Step 3: Run diagnostics for full picture
sudo patchmon-agent diagnostics
# Step 4: Test connectivity explicitly
sudo patchmon-agent ping
# Step 5: If needed, restart with debug logging temporarily
sudo systemctl stop patchmon-agent
sudo patchmon-agent serve --log-level debug
# (Ctrl+C to stop, then restart the service normally)
sudo systemctl start patchmon-agent
Manual Reporting
While the agent sends reports automatically on its configured interval, you can trigger a report at any time:
# Send a report immediately
sudo patchmon-agent report
This is useful after:
- Making system changes (installing/removing packages)
- Verifying the agent can communicate after a network change
- Testing after reconfiguring the agent
The report command also triggers integration data collection (Docker, compliance) and checks for agent updates, identical to a scheduled report.
Inspecting Report Data
To see exactly what the agent collects without sending anything:
# Full JSON output
sudo patchmon-agent report --json
# Pretty-print with jq
sudo patchmon-agent report --json | jq .
# Just the package count and update summary
sudo patchmon-agent report --json | jq '{
total_packages: (.packages | length),
needs_update: [.packages[] | select(.needsUpdate)] | length,
security_updates: [.packages[] | select(.isSecurityUpdate)] | length,
hostname: .hostname,
os: "\(.osType) \(.osVersion)"
}'
Configuration Management
For comprehensive documentation on all configuration parameters, see the Agent Configuration Reference (config.yml).
Quick Configuration Tasks
View current config:
sudo patchmon-agent config show
Set or change API credentials:
sudo patchmon-agent config set-api <API_ID> <API_KEY> <SERVER_URL>
Edit config file directly:
sudo nano /etc/patchmon/config.yml
sudo systemctl restart patchmon-agent # restart to apply changes
When do changes require a restart?
| Change | Restart Needed? |
|---|---|
patchmon_server |
Yes |
log_level |
Yes |
skip_ssl_verify |
Yes |
update_interval |
No (synced from server via WebSocket) |
integrations.docker |
No (synced from server) |
integrations.compliance |
No (synced from server) |
integrations.ssh-proxy-enabled |
Yes (manual config only) |
Credentials (api_id / api_key) |
Yes |
Agent Updates
How Auto-Update Works
The agent checks for updates in two ways:
- After each report — the agent queries the server for the latest version and updates automatically if one is available
- Server-initiated — the server can push an
update_notificationorupdate_agentcommand via WebSocket
When an update is detected:
- The new binary is downloaded from the PatchMon server
- SHA-256 hash is verified against the server-provided hash (mandatory)
- The current binary is backed up (last 3 backups are kept)
- The new binary replaces the old one atomically
- The service is restarted via a helper script
Manual Update
# Check what version is available
sudo patchmon-agent check-version
# Apply the update
sudo patchmon-agent update-agent
Update Safety Features
- Hash verification — refuses to install if the binary hash does not match
- Update loop prevention — blocks re-updates within 5 minutes of a previous update
- Automatic backup — creates a timestamped backup before replacing the binary
- Rollback — if the new binary fails validation, the update is aborted
- Version verification — checks that the downloaded binary reports the expected version
Backup Files
Update backups are stored alongside the binary:
/usr/local/bin/patchmon-agent # current binary
/usr/local/bin/patchmon-agent.backup.20260212_143000 # backup from update
/usr/local/bin/patchmon-agent.backup.20260210_090000 # older backup
/usr/local/bin/patchmon-agent.backup.20260201_120000 # oldest backup (3 kept)
The agent automatically removes backups beyond the most recent 3.
Agent Removal
There are two methods to remove the PatchMon agent from a host.
Method 1: Server-Provided Removal Script (Recommended)
curl -s https://patchmon.example.com/api/v1/hosts/remove | sudo sh
This script handles everything:
- Stops the service (systemd, OpenRC, or crontab)
- Removes the service file and reloads the daemon
- Kills any remaining agent processes
- Removes the agent binary and legacy scripts
- Removes configuration files and directories (
/etc/patchmon/) - Removes log files
- Cleans up crontab entries
Options:
| Environment Variable | Default | Description |
|---|---|---|
REMOVE_BACKUPS |
0 |
Set to 1 to also remove backup files |
SILENT |
not set | Set to 1 for silent mode (minimal output) |
Examples:
# Standard removal (preserves backups)
curl -s https://patchmon.example.com/api/v1/hosts/remove | sudo sh
# Remove everything including backups
curl -s https://patchmon.example.com/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 sh
# Silent removal (for automation)
curl -s https://patchmon.example.com/api/v1/hosts/remove | sudo SILENT=1 sh
# Silent removal with backup cleanup
curl -s https://patchmon.example.com/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 SILENT=1 sh
Method 2: Manual Removal
If the server is unreachable, you can remove the agent manually:
# 1. Stop and disable the service
sudo systemctl stop patchmon-agent
sudo systemctl disable patchmon-agent
sudo rm -f /etc/systemd/system/patchmon-agent.service
sudo systemctl daemon-reload
# or for OpenRC:
sudo rc-service patchmon-agent stop
sudo rc-update del patchmon-agent default
sudo rm -f /etc/init.d/patchmon-agent
# 2. Kill any remaining processes
sudo pkill -f patchmon-agent
# 3. Remove the binary and backups
sudo rm -f /usr/local/bin/patchmon-agent
sudo rm -f /usr/local/bin/patchmon-agent.backup.*
# 4. Remove configuration and logs
sudo rm -rf /etc/patchmon/
# 5. Remove crontab entries (if any)
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
# 6. Verify removal
which patchmon-agent # should return nothing
ls /etc/patchmon/ 2>/dev/null # should show "No such file or directory"
systemctl status patchmon-agent 2>&1 | head -1 # should show "not found"
Important: Removing the agent from the host does not remove the host entry from PatchMon. To fully decommission a host, also delete it from the PatchMon web UI (Hosts page).
Common Troubleshooting
Agent Shows "Pending" in PatchMon
The host was created but the agent has not yet sent its first report.
# Check service is running
sudo systemctl status patchmon-agent
# Test connectivity
sudo patchmon-agent ping
# If ping fails, check the server URL
sudo patchmon-agent config show
# Force an immediate report
sudo patchmon-agent report
Agent Shows "Offline" in PatchMon
The agent's WebSocket connection is down.
# Check if the service is running
sudo systemctl is-active patchmon-agent
# If not running, check why it stopped
sudo journalctl -u patchmon-agent -n 50 --no-pager
# Restart the service
sudo systemctl restart patchmon-agent
"Permission Denied" Errors
# All agent commands require root
sudo patchmon-agent <command>
# Verify file permissions
ls -la /etc/patchmon/config.yml # should be -rw------- root
ls -la /etc/patchmon/credentials.yml # should be -rw------- root
ls -la /usr/local/bin/patchmon-agent # should be -rwxr-xr-x root
"Credentials File Not Found"
# Check if credentials exist
ls -la /etc/patchmon/credentials.yml
# If missing, reconfigure
sudo patchmon-agent config set-api <API_ID> <API_KEY> <SERVER_URL>
"Connectivity Test Failed"
# Run full diagnostics
sudo patchmon-agent diagnostics
# Test network connectivity manually
curl -I https://patchmon.example.com
# Check DNS resolution
nslookup patchmon.example.com
# or
dig patchmon.example.com
# Check firewall rules
sudo iptables -L -n | grep -i drop
SSL Certificate Errors
# For self-signed certificates in non-production environments:
# Edit /etc/patchmon/config.yml
skip_ssl_verify: true
# Then restart
sudo systemctl restart patchmon-agent
Warning:
skip_ssl_verifyis blocked when thePATCHMON_ENVenvironment variable is set toproduction. This is a security measure to prevent disabling TLS verification in production.
Service Keeps Restarting
Check for crash loops:
# See restart count and recent failures
sudo systemctl status patchmon-agent
# Check logs around restart times
sudo journalctl -u patchmon-agent --since "30 minutes ago" --no-pager
# Common causes:
# - Invalid config.yml (syntax error)
# - Invalid credentials
# - Server unreachable (agent retries but logs errors)
Agent Not Auto-Updating
# Check current version
patchmon-agent version
# Check if update is available
sudo patchmon-agent check-version
# Check if auto-update was recently performed
ls -la /etc/patchmon/.last_update_timestamp
# Try manual update
sudo patchmon-agent update-agent
# Check for update loop prevention (5-minute cooldown)
# If you see "update was performed X ago", wait 5 minutes
Architecture and Supported Platforms
Supported Architectures
| Architecture | Binary Name | Common Devices |
|---|---|---|
amd64 |
patchmon-agent-linux-amd64 |
Standard servers, VMs, most cloud instances |
arm64 |
patchmon-agent-linux-arm64 |
ARM servers, Raspberry Pi 4+, AWS Graviton |
arm (v6/v7) |
patchmon-agent-linux-arm |
Raspberry Pi 2/3, older ARM boards |
386 |
patchmon-agent-linux-386 |
32-bit x86 systems (legacy) |
Supported Operating Systems
| Distribution | Init System | Package Manager | Notes |
|---|---|---|---|
| Ubuntu | systemd | apt | All LTS versions supported |
| Debian | systemd | apt | 10+ |
| CentOS | systemd | yum/dnf | 7+ |
| RHEL | systemd | yum/dnf | 7+ |
| Rocky Linux | systemd | dnf | All versions |
| AlmaLinux | systemd | dnf | All versions |
| Fedora | systemd | dnf | Recent versions |
| Alpine Linux | OpenRC | apk | 3.x+ |
Resource Usage
The agent is lightweight:
| Resource | Typical Usage |
|---|---|
| Memory | ~15-30 MB RSS |
| CPU | Near zero when idle; brief spikes during report collection |
| Disk | ~15 MB (binary) + logs |
| Network | WebSocket keepalive (~1 KB/min); report payloads vary by package count |
See Also:
- Agent Configuration Reference (config.yml) — detailed documentation on every config parameter
- Proxmox LXC Auto-Enrollment Guide — bulk agent deployment on Proxmox
- Integration API Documentation — API endpoints used by the agent
config.yml Mangement and parameters
Overview
The PatchMon agent is configured through a YAML configuration file located at /etc/patchmon/config.yml. This file controls how the agent communicates with the PatchMon server, where logs are stored, which integrations are active, and other runtime behaviour. A separate credentials file (/etc/patchmon/credentials.yml) stores the host's API authentication details.
Both files are owned by root and set to 600 permissions (read/write by owner only) to protect sensitive information.
File Locations
| File | Default Path | Purpose |
|---|---|---|
| Configuration | /etc/patchmon/config.yml |
Agent settings, server URL, integrations |
| Credentials | /etc/patchmon/credentials.yml |
API ID and API Key for host authentication |
| Log File | /etc/patchmon/logs/patchmon-agent.log |
Agent log output |
| Cron File | /etc/cron.d/patchmon-agent |
Scheduled reporting (fallback for non-systemd systems) |
Full Configuration Reference
Below is a complete config.yml with all available parameters, their defaults, and descriptions:
# PatchMon Agent Configuration
# Location: /etc/patchmon/config.yml
# ─── Server Connection ───────────────────────────────────────────────
# The URL of the PatchMon server this agent reports to.
# Required. Must start with http:// or https://
patchmon_server: "https://patchmon.example.com"
# API version to use when communicating with the server.
# Default: "v1" — do not change unless instructed.
api_version: "v1"
# ─── File Paths ──────────────────────────────────────────────────────
# Path to the credentials file containing api_id and api_key.
# Default: "/etc/patchmon/credentials.yml"
credentials_file: "/etc/patchmon/credentials.yml"
# Path to the agent log file. Logs are rotated automatically
# (max 10 MB per file, 5 backups, 14-day retention, compressed).
# Default: "/etc/patchmon/logs/patchmon-agent.log"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
# ─── Logging ─────────────────────────────────────────────────────────
# Log verbosity level.
# Options: "debug", "info", "warn", "error"
# Default: "info"
log_level: "info"
# ─── SSL / TLS ───────────────────────────────────────────────────────
# Skip SSL certificate verification when connecting to the server.
# Set to true only if using self-signed certificates.
# Default: false
skip_ssl_verify: false
# ─── Reporting Schedule ──────────────────────────────────────────────
# How often (in minutes) the agent sends a full report to the server.
# This value is synced from the server on startup. If the server has
# a different value, the agent updates config.yml automatically.
# Default: 60
update_interval: 60
# Report offset (in seconds). Automatically calculated from the host's
# api_id to stagger reporting across hosts and avoid thundering-herd.
# You should not need to set this manually — the agent calculates and
# persists it automatically.
# Default: 0 (auto-calculated on first run)
report_offset: 0
# ─── Integrations ────────────────────────────────────────────────────
# Integration toggles control optional agent features.
# Most integrations can be toggled from the PatchMon UI and the server
# will push the change to the agent via WebSocket. The agent then
# updates config.yml and restarts the relevant service.
#
# EXCEPTION: ssh-proxy-enabled CANNOT be pushed from the server.
# It must be manually set in this file (see below).
integrations:
# Docker integration — monitors containers, images, volumes, networks.
# Can be toggled from the PatchMon UI (Settings → Integrations).
# Default: false
docker: false
# Compliance integration — OpenSCAP and Docker Bench security scanning.
# Three modes:
# false — Disabled. No scans run.
# "on-demand" — Scans only run when triggered from the PatchMon UI.
# true — Enabled with automatic scheduled scans every report cycle.
# Can be toggled from the PatchMon UI.
# Default: "on-demand"
compliance: "on-demand"
# SSH Proxy — allows browser-based SSH sessions through the agent.
# SECURITY: This setting can ONLY be enabled by manually editing
# this file. It cannot be pushed from the server to the agent.
# This is intentional — enabling remote shell access should require
# deliberate action by someone with root access on the host.
# Default: false
ssh-proxy-enabled: false
Parameters In Detail
patchmon_server
| Type | String (URL) |
| Required | Yes |
| Default | None — must be provided |
| Example | https://patchmon.example.com |
The full URL of the PatchMon server. Must include the protocol (http:// or https://). Do not include a trailing slash or path.
api_version
| Type | String |
| Required | No |
| Default | v1 |
The API version string appended to API calls. Leave as v1 unless directed otherwise by PatchMon documentation or release notes.
credentials_file
| Type | String (file path) |
| Required | No |
| Default | /etc/patchmon/credentials.yml |
Path to the YAML file containing the host's api_id and api_key. The credentials file has this structure:
api_id: "patchmon_abc123def456"
api_key: "your_api_key_here"
log_file
| Type | String (file path) |
| Required | No |
| Default | /etc/patchmon/logs/patchmon-agent.log |
Path to the agent's log file. The directory is created automatically if it does not exist. Logs are rotated using the following policy:
- Max file size: 10 MB
- Max backups: 5 rotated files
- Max age: 14 days
- Compression: Enabled (gzip)
log_level
| Type | String |
| Required | No |
| Default | info |
| Options | debug, info, warn, error |
Controls the verbosity of agent logging. Use debug for troubleshooting — it includes API request/response bodies and detailed execution flow. Can also be overridden at runtime with the --log-level CLI flag.
skip_ssl_verify
| Type | Boolean |
| Required | No |
| Default | false |
When true, the agent skips TLS certificate verification when connecting to the PatchMon server. Use this only for internal/testing environments with self-signed certificates. Not recommended for production.
update_interval
| Type | Integer (minutes) |
| Required | No |
| Default | 60 |
How frequently the agent sends a full system report (installed packages, updates, etc.) to the server. This value is synced from the server — if you change the global or per-host reporting interval in the PatchMon UI, the agent will update this value in config.yml automatically on its next startup or when it receives a settings update via WebSocket.
If the value is 0 or negative, the agent falls back to the default of 60 minutes.
report_offset
| Type | Integer (seconds) |
| Required | No |
| Default | 0 (auto-calculated) |
A stagger offset calculated from the host's api_id and the current update_interval. This ensures that agents across your fleet do not all report at the exact same moment (avoiding a thundering-herd problem on the server).
You should not set this manually. The agent calculates it on first run and saves it. If the update_interval changes, the offset is recalculated automatically.
integrations
A map of integration names to their enabled/disabled state. See the Integrations section below for details on each.
Integrations
Docker (docker)
| Type | Boolean |
| Default | false |
| Server-pushable | ✅ Yes |
When enabled, the agent monitors Docker containers, images, volumes, and networks on the host. It sends real-time container status events and periodic inventory snapshots to the PatchMon server.
Requirements: Docker must be installed and the Docker socket must be accessible.
Toggle from UI: Go to a host's detail page → Integrations tab → Toggle Docker on/off. The server pushes the change to the agent via WebSocket, the agent updates config.yml, and the service restarts automatically.
Compliance (compliance)
| Type | Boolean or String |
| Default | "on-demand" |
| Server-pushable | ✅ Yes |
| Valid values | false, "on-demand", true |
Controls OpenSCAP and Docker Bench security compliance scanning.
| Value | Behaviour |
|---|---|
false |
Compliance scanning is fully disabled. No scans run. |
"on-demand" |
Scans only run when manually triggered from the PatchMon UI. Tools are installed but no automatic scheduled scans occur. |
true |
Fully enabled. Scans run automatically on every report cycle in addition to being available on-demand. |
When first enabled, the agent automatically installs the required compliance tools (OpenSCAP, SSG content packages, Docker Bench image if Docker is also enabled).
SSH Proxy (ssh-proxy-enabled)
| Type | Boolean |
| Default | false |
| Server-pushable | ❌ No — manual edit required |
Enables browser-based SSH terminal sessions that are proxied through the PatchMon agent. When a user opens the SSH terminal in the PatchMon UI, the server sends the SSH connection request to the agent via WebSocket, and the agent establishes a local SSH connection on behalf of the user.
Why SSH Proxy Requires Manual Configuration
This is a deliberate security design decision. Enabling SSH proxy effectively allows remote shell access to the host through the PatchMon agent. Unlike Docker or compliance integrations, this has direct security implications:
- It opens an SSH connection path through the agent
- It could be exploited if a PatchMon server or user account were compromised
- The host administrator should make an informed, deliberate choice to enable it
For these reasons, ssh-proxy-enabled cannot be toggled from the PatchMon UI or pushed from the server. If the server attempts to initiate an SSH proxy session while this is disabled, the agent rejects the request and returns an error message explaining how to enable it.
How to Enable SSH Proxy
- SSH into the host where the PatchMon agent is installed
- Open the config file:
sudo nano /etc/patchmon/config.yml
- Find the
integrationssection and changessh-proxy-enabledtotrue:
integrations:
docker: false
compliance: "on-demand"
ssh-proxy-enabled: true # ← Change from false to true
- Save the file and restart the agent:
# Systemd
sudo systemctl restart patchmon-agent.service
# OpenRC (Alpine)
sudo rc-service patchmon-agent restart
- The SSH terminal feature is now available for this host in the PatchMon UI
How to Disable SSH Proxy
Set ssh-proxy-enabled back to false in config.yml and restart the agent service. Existing SSH sessions will be terminated.
How config.yml Is Generated
Initial Generation (Installation)
The config.yml file is created during agent installation by the patchmon_install.sh script. The installer generates a fresh config with:
patchmon_serverset to the server URL used during installationskip_ssl_verifyset based on whether-kcurl flags were used- All integrations defaulted to
false(Docker, SSH proxy) or"disabled"(compliance) - Standard file paths for credentials and logs
# What the installer generates:
cat > /etc/patchmon/config.yml << EOF
# PatchMon Agent Configuration
# Generated on $(date)
patchmon_server: "https://patchmon.example.com"
api_version: "v1"
credentials_file: "/etc/patchmon/credentials.yml"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
log_level: "info"
skip_ssl_verify: false
integrations:
docker: false
compliance: "disabled"
ssh-proxy-enabled: false
EOF
chmod 600 /etc/patchmon/config.yml
Reinstallation Behaviour
If the agent is reinstalled on a host that already has a working configuration:
- The installer checks if the existing configuration is valid by running
patchmon-agent ping - If the ping succeeds, the installer exits without overwriting — the existing configuration is preserved
- If the ping fails (or the binary is missing), the installer:
- Creates a timestamped backup:
config.yml.backup.YYYYMMDD_HHMMSS - Keeps only the last 3 backups (older ones are deleted)
- Writes a fresh
config.yml
- Creates a timestamped backup:
This means a reinstall on a healthy agent is safe and will not destroy your configuration.
How config.yml Is Regenerated / Updated at Runtime
The agent updates config.yml automatically in several scenarios. These are in-place updates — the agent reads the file, modifies the relevant field, and writes it back. Your other settings (including ssh-proxy-enabled) are preserved.
Server-Driven Updates
| Trigger | What Changes | How |
|---|---|---|
| Agent startup | update_interval, report_offset |
Agent fetches the current interval from the server. If it differs from config, the agent updates config.yml. |
| Agent startup | integrations.docker, integrations.compliance |
Agent fetches integration status from the server. If it differs from config, the agent updates config.yml. |
WebSocket: settings_update |
update_interval, report_offset |
Server pushes a new interval. Agent saves it and recalculates the report offset. |
WebSocket: integration_toggle |
integrations.* (except SSH proxy) |
Server pushes a toggle for Docker or compliance. Agent saves the change and restarts the relevant service. |
Agent-Calculated Updates
| Trigger | What Changes | How |
|---|---|---|
| First run | report_offset |
Calculated from api_id hash and update_interval to stagger reports. |
| Interval change | report_offset |
Recalculated whenever update_interval changes. |
CLI: config set-api |
patchmon_server, credentials |
Running patchmon-agent config set-api overwrites the server URL and saves new credentials. |
What Is Never Changed Automatically
| Parameter | Why |
|---|---|
ssh-proxy-enabled |
Security — requires manual host-level action |
log_level |
Only changed by manual edit or --log-level CLI flag |
log_file |
Only changed by manual edit |
credentials_file |
Only changed by manual edit or config set-api |
skip_ssl_verify |
Only changed by manual edit |
Important: How SaveConfig Works
When the agent calls SaveConfig() internally, it writes all parameters back to the file. This means:
- Your
ssh-proxy-enabled: truesetting is preserved across server-driven updates - New integrations added in agent updates are automatically added to the file with their defaults (you'll see them appear after an agent update)
- The file format may be slightly reorganised by the YAML serialiser (key ordering may change), but all values are preserved
CLI Configuration Commands
The agent provides CLI commands for configuration management:
View Current Configuration
sudo patchmon-agent config show
Output:
Configuration:
Server: https://patchmon.example.com
Agent Version: 1.4.0
Config File: /etc/patchmon/config.yml
Credentials File: /etc/patchmon/credentials.yml
Log File: /etc/patchmon/logs/patchmon-agent.log
Log Level: info
Credentials:
API ID: patchmon_abc123def456
API Key: Set ✅
Set API Credentials
sudo patchmon-agent config set-api <API_ID> <API_KEY> <SERVER_URL>
Example:
sudo patchmon-agent config set-api patchmon_1a2b3c4d abcdef123456 https://patchmon.example.com
This command:
- Validates the server URL format
- Saves the server URL to
config.yml - Saves the credentials to
credentials.yml - Tests connectivity with a ping to the server
- Reports success or failure
Custom Config File Path
All commands support a --config flag to use an alternative config file:
sudo patchmon-agent --config /path/to/custom/config.yml serve
Credentials File (credentials.yml)
The credentials file is separate from the config file for security isolation. It contains:
api_id: "patchmon_abc123def456"
api_key: "your_api_key_here"
- Permissions:
600(root read/write only) - Written using atomic rename: The agent writes to a temp file first, then atomically renames it. This prevents partial writes or race conditions.
- Never contains the hashed key: The plain-text API key is stored here; the server stores only the bcrypt hash.
Troubleshooting
Config File Missing
If /etc/patchmon/config.yml does not exist, the agent uses built-in defaults. This means it will not know which server to connect to. Reinstall the agent or create the file manually.
Config File Permissions
# Check permissions (should be 600, owned by root)
ls -la /etc/patchmon/config.yml
# Fix if needed
sudo chmod 600 /etc/patchmon/config.yml
sudo chown root:root /etc/patchmon/config.yml
SSH Proxy Not Working
If the SSH terminal in the PatchMon UI shows an error like:
SSH proxy is not enabled. To enable SSH proxy, edit the file /etc/patchmon/config.yml...
This means ssh-proxy-enabled is set to false (the default). Follow the How to Enable SSH Proxy instructions above.
Config Gets Overwritten
If you notice settings being changed unexpectedly, check:
- Server sync: The
update_intervaland integration toggles (Docker, compliance) are synced from the server on startup and via WebSocket. Changes made in the PatchMon UI will override local values for these fields. - Agent updates: After an agent update, new integration keys may appear in the file with default values.
- Reinstallation: A reinstall only overwrites config if the existing ping test fails.
Your ssh-proxy-enabled, log_level, skip_ssl_verify, and file path settings are never overwritten by server sync.
Viewing Debug Logs
# Temporarily enable debug logging
sudo patchmon-agent --log-level debug serve
# Or set permanently in config.yml
sudo nano /etc/patchmon/config.yml
# Change: log_level: "debug"
# Then restart the service
sudo systemctl restart patchmon-agent.service
Example Configurations
Minimal Configuration
patchmon_server: "https://patchmon.example.com"
All other values use defaults. The agent will function with just the server URL (and valid credentials in credentials.yml).
Full Configuration with SSH Proxy Enabled
patchmon_server: "https://patchmon.internal.company.com"
api_version: "v1"
credentials_file: "/etc/patchmon/credentials.yml"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
log_level: "info"
skip_ssl_verify: false
update_interval: 30
report_offset: 847
integrations:
docker: true
compliance: "on-demand"
ssh-proxy-enabled: true
Self-Signed SSL with Debug Logging
patchmon_server: "https://patchmon.lab.local"
api_version: "v1"
credentials_file: "/etc/patchmon/credentials.yml"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
log_level: "debug"
skip_ssl_verify: true
update_interval: 60
integrations:
docker: false
compliance: false
ssh-proxy-enabled: false
Known issues & troubleshooting
PatchMon Rate Limiting Guide
PatchMon implements rate limiting to protect your instance from abuse, brute force attacks, and excessive load. This guide explains how rate limiting works, how to identify rate limit issues, and how to adjust limits for your deployment.
Understanding HTTP 429 Errors
When rate limits are exceeded, the API returns an HTTP 429 status code with the message "Too Many Requests". This indicates that the client has sent too many requests in a given time window and should retry after the specified period.
Default Rate Limits
PatchMon uses three different rate limiters for different types of endpoints:
General API Rate Limit
Applies to most API endpoints.
| Setting | Default Value | Description |
|---|---|---|
RATE_LIMIT_WINDOW_MS |
900000 |
Time window in milliseconds (15 minutes) |
RATE_LIMIT_MAX |
5000 |
Maximum requests allowed per window |
Default: 5000 requests per 15 minutes
Authentication Rate Limit
Applies to authentication endpoints (/api/*/auth).
| Setting | Default Value | Description |
|---|---|---|
AUTH_RATE_LIMIT_WINDOW_MS |
600000 |
Time window in milliseconds (10 minutes) |
AUTH_RATE_LIMIT_MAX |
500 |
Maximum auth requests allowed per window |
Default: 500 requests per 10 minutes
Agent API Rate Limit
Applies to agent/host management endpoints (/api/*/hosts).
| Setting | Default Value | Description |
|---|---|---|
AGENT_RATE_LIMIT_WINDOW_MS |
60000 |
Time window in milliseconds (1 minute) |
AGENT_RATE_LIMIT_MAX |
1000 |
Maximum agent requests allowed per window |
Default: 1000 requests per minute
Identifying Rate Limit Issues
Symptoms
- HTTP 429 errors in application logs
- Agents failing to report or update
- Users unable to authenticate or access the dashboard
- API requests timing out or failing
Checking Rate Limit Status
The backend includes standard rate limit headers in API responses that help you monitor your usage:
| Header | Description |
|---|---|
RateLimit-Limit |
Maximum requests allowed in the current window |
RateLimit-Remaining |
Number of requests remaining in current window |
RateLimit-Reset |
Unix timestamp when the rate limit window resets |
You can inspect these headers using browser developer tools or command-line tools like curl:
curl -I -H "Authorization: Bearer YOUR_TOKEN" http://your-patchmon-instance/api/v1/hosts
Error Response Format
When rate limited, the API returns a JSON response:
{
"error": "Too many requests from this IP, please try again later.",
"retryAfter": 900
}
The retryAfter field indicates how many seconds to wait before retrying.
Adjusting Rate Limits
For Docker Deployments
Method 1: Environment Variables in docker-compose.yml
Edit your docker-compose.yml file to add or modify rate limiting environment variables:
services:
backend:
image: ghcr.io/patchmon/patchmon-backend:latest
environment:
# General API rate limit
RATE_LIMIT_WINDOW_MS: 900000
RATE_LIMIT_MAX: 10000 # Increase from 5000 to 10000
# Authentication rate limit
AUTH_RATE_LIMIT_WINDOW_MS: 600000
AUTH_RATE_LIMIT_MAX: 1000 # Increase from 500 to 1000
# Agent API rate limit
AGENT_RATE_LIMIT_WINDOW_MS: 60000
AGENT_RATE_LIMIT_MAX: 5000 # Increase from 1000 to 5000
# ... other environment variables
After modifying the file, restart the backend service:
docker compose restart backend
Method 2: Environment File
Alternatively, create a .env file and reference it in your compose file:
# .env file
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=10000
AUTH_RATE_LIMIT_WINDOW_MS=600000
AUTH_RATE_LIMIT_MAX=1000
AGENT_RATE_LIMIT_WINDOW_MS=60000
AGENT_RATE_LIMIT_MAX=5000
Reference it in docker-compose.yml:
services:
backend:
env_file: .env
# ... other configuration
For Native Deployments
If you're running PatchMon natively (without Docker), set the environment variables in your backend's .env file:
# backend/.env
# Rate Limiting (times in milliseconds)
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=10000
AUTH_RATE_LIMIT_WINDOW_MS=600000
AUTH_RATE_LIMIT_MAX=1000
AGENT_RATE_LIMIT_WINDOW_MS=60000
AGENT_RATE_LIMIT_MAX=5000
Then restart your backend service:
# Using systemd
sudo systemctl restart patchmon-backend
# Or using PM2
pm2 restart patchmon-backend
Recommended Settings for Different Deployment Sizes
Small Deployment (1-50 hosts)
The default settings are appropriate:
RATE_LIMIT_MAX: 5000
AUTH_RATE_LIMIT_MAX: 500
AGENT_RATE_LIMIT_MAX: 1000
Medium Deployment (51-200 hosts)
Increase agent limits:
RATE_LIMIT_MAX: 10000
AUTH_RATE_LIMIT_MAX: 1000
AGENT_RATE_LIMIT_MAX: 3000
Large Deployment (201-1000 hosts)
Significantly increase all limits:
RATE_LIMIT_MAX: 25000
AUTH_RATE_LIMIT_MAX: 2000
AGENT_RATE_LIMIT_MAX: 10000
Very Large Deployment (1000+ hosts)
Consider implementing a more sophisticated approach:
RATE_LIMIT_MAX: 50000
AUTH_RATE_LIMIT_MAX: 5000
AGENT_RATE_LIMIT_MAX: 25000
Best Practices
1. Monitor Your Usage
Regularly check your rate limit headers and application logs to understand your actual usage patterns. This helps you set appropriate limits.
2. Gradual Increases
When adjusting limits, increase them gradually and monitor the impact on system performance. Sudden large increases may impact server resources.
3. Consider Peak Times
Set limits based on peak usage times, not average usage. Account for:
- Agent check-in schedules (if many agents report simultaneously)
- User login times (e.g., start of business day)
- Automated script executions
4. Load Balancing
For very large deployments, consider running multiple backend instances behind a load balancer, with each instance having its own rate limits.
5. Document Your Changes
Keep a record of rate limit changes and the reasons for them. This helps with troubleshooting and capacity planning.
Security Considerations
Do Not Disable Rate Limiting
Rate limiting is a critical security feature. Disabling it entirely (by setting extremely high limits) exposes your instance to:
- Brute Force Attacks: Attackers attempting to guess passwords
- DDoS Attacks: Overwhelming your server with requests
- Resource Exhaustion: Excessive requests consuming server resources
- API Abuse: Malicious or poorly configured clients hammering endpoints
Authentication Endpoints
Be especially careful with authentication rate limits (AUTH_RATE_LIMIT_MAX). Low limits here help prevent:
- Password guessing attacks
- Credential stuffing attempts
- Session hijacking attempts
Recommendation: Only increase authentication limits if you have legitimate high-volume authentication needs.
Behind a Reverse Proxy
If PatchMon is behind a reverse proxy (nginx, Caddy, Apache, etc.):
- Ensure the
TRUST_PROXYenvironment variable is correctly set - The reverse proxy should pass the real client IP
- Consider implementing rate limiting at the reverse proxy level as an additional layer
TRUST_PROXY: true # or specific proxy IPs
IP-Based Limiting
PatchMon's rate limiting is IP-based by default. This means:
- Multiple users behind the same NAT/proxy share a rate limit
- In office environments, all users may share limits
- Consider this when setting limits for your deployment
Troubleshooting
Problem: Agents Consistently Hit Rate Limits
Symptoms: Agents fail to report, 429 errors in agent logs
Solutions:
- Increase
AGENT_RATE_LIMIT_MAX - Stagger agent check-in times
- Reduce agent reporting frequency
- Check for misconfigured agents sending excessive requests
Problem: Users Cannot Log In (429 on Auth Endpoints)
Symptoms: Login failures, authentication errors
Solutions:
- Check for brute force attacks in logs
- If legitimate traffic, increase
AUTH_RATE_LIMIT_MAX - Consider implementing IP whitelisting for trusted networks
- Check if automated scripts are hammering the auth endpoints
Problem: Dashboard API Calls Failing
Symptoms: Dashboard loading issues, 429 errors in browser console
Solutions:
- Increase
RATE_LIMIT_MAX - Check for inefficient frontend code making excessive requests
- Implement client-side caching
- Review polling intervals in the frontend
Problem: Rate Limits Reset Too Slowly
Symptoms: Users/agents locked out for extended periods
Solutions:
- Reduce
*_WINDOW_MSvalues for faster reset - Balance between security and usability
- Consider exponential backoff in clients
Monitoring and Alerting
Logs to Monitor
Watch for these patterns in your logs:
# Check for rate limit hits
docker compose logs backend | grep "Too many requests"
# Check which IPs are being rate limited
docker compose logs backend | grep "429"
Set Up Alerts
Consider setting up alerts when rate limiting occurs frequently:
- High number of 429 responses (indicates limits too low or potential attack)
- Specific users/agents consistently hitting limits
- Sudden spikes in rate limit violations
Metrics to Track
- Number of 429 responses per hour/day
- Which endpoints are most rate-limited
- Which IPs/users trigger rate limits
- Average rate limit usage (via headers)
Additional Resources
- Express Rate Limit Documentation
- OWASP Rate Limiting Best Practices
- PatchMon GitHub Repository
- PatchMon Documentation
Support
If you continue to experience rate limiting issues after following this guide:
- Check the GitHub Issues for similar problems
- Review your application logs for specific error patterns
- Open a new issue with:
- Your deployment size (number of hosts/users)
- Current rate limit settings
- Frequency of 429 errors
- Relevant log snippets
Last Updated: October 2025
PatchMon Version: 1.3.0+
Errors on dashboard after updating using Proxmox-community scripts
There seems to be an issue where some people are facing a problem where when they upgrade then it's giving them errors on the dashboard such as "network error" or others, that relates to the fact that the frontend built files is unable to communicate with the PatchMon server.
This seems to be due to the frontend environment file containing a variable VITE_API_URL
Once you remove or comment this out and go back to the PatchMon installation directory where you're able to see both frontend & backend directory then run npm run build
After this it should start working again.
As a rule of thumb if the VITE_API_URL is to be set then set it the same as your CORS_ORIGIN.
Ideally keep this unset and build the files.
Software Architecture
This chapter has documentation on the software architecture
PatchMon Architecture Documentation
Table of Contents
- System Overview
- Nginx Reverse Proxy
- BullMQ Queue System
- Database Schema
- API Credential Scoping
- WebSocket Communication
- Agent Communication Flow
- Authentication & Authorization
System Overview
mermaid
graph TB
subgraph "Client Layer"
Browser[Web Browser]
Agent[PatchMon Agent<br/>Go Binary]
end
subgraph "Reverse Proxy Layer"
Nginx[Nginx<br/>Port 80/443]
end
subgraph "Application Layer"
Frontend[React Frontend<br/>Vite + Tailwind]
Backend[Node.js Backend<br/>Express Server<br/>Port 3001]
end
subgraph "Queue System"
Redis[(Redis<br/>Queue Storage)]
BullMQ[BullMQ Queues]
Workers[BullMQ Workers]
end
subgraph "Data Layer"
PostgreSQL[(PostgreSQL<br/>Primary Database)]
end
subgraph "External Services"
OIDC[OIDC Provider<br/>Optional]
DNS[DNS Server<br/>Version Check<br/>server.vcheck.patchmon.net]
end
Browser -->|HTTPS/HTTP| Nginx
Agent -->|HTTPS/HTTP<br/>WebSocket| Nginx
Nginx -->|"Proxy /api/*"| Backend
Nginx -->|"Proxy /bullboard"| Backend
Nginx -->|Serve Static| Frontend
Backend -->|Read/Write| PostgreSQL
Backend -->|Queue Jobs| BullMQ
BullMQ -->|Store Jobs| Redis
Workers -->|Process Jobs| BullMQ
Workers -->|Read/Write| PostgreSQL
Backend -->|WebSocket| Agent
Backend -->|OAuth| OIDC
Backend -->|DNS TXT Query| DNS
Nginx Reverse Proxy
Request Routing Flow
graph LR
Client[Client Request] --> Nginx{Nginx<br/>Port 80/443}
Nginx -->|"Location: /"| Frontend[Frontend<br/>Static Files<br/>/usr/share/nginx/html]
Nginx -->|"Location: /api/*"| Backend[Backend API<br/>http://backend:3001]
Nginx -->|"Location: /bullboard"| Backend[Bull Board UI<br/>http://backend:3001/bullboard]
Nginx -->|"Location: /assets/*"| Assets[Custom Branding<br/>/usr/share/nginx/html/assets/]
Nginx -->|"Location: /health"| Health[Health Check<br/>200 OK]
Backend -->|WebSocket Upgrade| WS[WebSocket Handler]
Nginx Configuration Details
Key Features:
- Port 3000 (Docker) or 80/443 (Production)
- SSL/TLS support with Let's Encrypt
- WebSocket upgrade support for real-time communication
- Security headers (HSTS, X-Frame-Options, CSP)
- Cookie passthrough for authentication
- IP forwarding (X-Real-IP, X-Forwarded-For)
- Large file uploads (10MB limit for API routes)
Location Blocks:
/- Frontend SPA (try_files for client-side routing)/api/*- Backend API proxy with WebSocket support/bullboard- Bull Board queue monitoring UI/assets/*- Custom branding assets (logos, favicons)/health- Health check endpoint
BullMQ Queue System
Queue Architecture
graph TB
subgraph "Queue Manager"
QM[QueueManager<br/>Singleton]
end
subgraph "Queues (BullMQ)"
Q1[version-update-check]
Q2[session-cleanup]
Q3[orphaned-repo-cleanup]
Q4[orphaned-package-cleanup]
Q5[docker-inventory-cleanup]
Q6[docker-image-update-check]
Q7[metrics-reporting]
Q8[system-statistics]
Q9[social-media-stats]
Q10[agent-commands]
Q11[alert-cleanup]
Q12[host-status-monitor]
end
subgraph "Workers"
W1[VersionUpdateCheck Worker]
W2[SessionCleanup Worker]
W3[OrphanedRepoCleanup Worker]
W4[OrphanedPackageCleanup Worker]
W5[DockerInventoryCleanup Worker]
W6[DockerImageUpdateCheck Worker]
W7[MetricsReporting Worker]
W8[SystemStatistics Worker]
W9[SocialMediaStats Worker]
W10[AgentCommands Worker]
W11[AlertCleanup Worker]
W12[HostStatusMonitor Worker]
end
subgraph "Storage Layer"
Redis[(Redis<br/>Job Storage)]
end
subgraph "Database"
DB[(PostgreSQL)]
end
QM --> Q1
QM --> Q2
QM --> Q3
QM --> Q4
QM --> Q5
QM --> Q6
QM --> Q7
QM --> Q8
QM --> Q9
QM --> Q10
QM --> Q11
QM --> Q12
Q1 --> Redis
Q2 --> Redis
Q3 --> Redis
Q4 --> Redis
Q5 --> Redis
Q6 --> Redis
Q7 --> Redis
Q8 --> Redis
Q9 --> Redis
Q10 --> Redis
Q11 --> Redis
Q12 --> Redis
W1 --> Q1
W2 --> Q2
W3 --> Q3
W4 --> Q4
W5 --> Q5
W6 --> Q6
W7 --> Q7
W8 --> Q8
W9 --> Q9
W10 --> Q10
W11 --> Q11
W12 --> Q12
W1 --> DB
W2 --> DB
W3 --> DB
W4 --> DB
W5 --> DB
W6 --> DB
W7 --> DB
W8 --> DB
W9 --> DB
W10 --> DB
W11 --> DB
W12 --> DB
W10 -->|WebSocket Commands| AgentWS[Agent WebSocket]
Queue Details
| Queue Name | Purpose | Schedule | Worker |
|---|---|---|---|
version-update-check |
Check for PatchMon updates | Daily | VersionUpdateCheck |
session-cleanup |
Clean expired user sessions | Hourly | SessionCleanup |
orphaned-repo-cleanup |
Remove unused repositories | Daily | OrphanedRepoCleanup |
orphaned-package-cleanup |
Remove unused packages | Daily | OrphanedPackageCleanup |
docker-inventory-cleanup |
Clean Docker inventory | Daily | DockerInventoryCleanup |
docker-image-update-check |
Check Docker image updates | Daily | DockerImageUpdateCheck |
metrics-reporting |
Report anonymous metrics | Daily | MetricsReporting |
system-statistics |
Generate system statistics | Hourly | SystemStatistics |
social-media-stats |
Fetch social media stats | On boot | SocialMediaStats |
agent-commands |
Send commands to agents | On demand | AgentCommands |
alert-cleanup |
Clean old alerts | Daily | AlertCleanup |
host-status-monitor |
Monitor host status | Every 5 min | HostStatusMonitor |
Worker Configuration
- Concurrency: 1 job at a time per worker
- Retry Policy: 3 attempts with exponential backoff
- Job Retention: 50 completed, 20 failed jobs
- Connection: Shared Redis connection pool
Version Check Mechanism
The version update check uses DNS TXT records instead of GitHub API to avoid rate limiting and provide a lightweight, fast version checking mechanism.
DNS Domains:
- Server version:
server.vcheck.patchmon.net - Agent version:
agent.vcheck.patchmon.net
How it works:
- Worker queries DNS TXT record for the appropriate domain
- DNS returns version string (e.g., "1.4.0") in TXT record
- Version is validated against semantic versioning format
- Compared with current version from
package.json(server) or agent binary - Update alerts created if newer version available
Benefits:
- No API rate limits
- Fast DNS lookups (cached by DNS resolvers)
- No authentication required
- Works even if GitHub API is unavailable
Implementation:
- Uses Node.js
dns.resolveTxt()for DNS queries - Validates version format:
^\d+\.\d+\.\d+ - Falls back to cached version from database if DNS lookup fails
Database Schema
Core Entity Relationships
erDiagram
users ||--o{ user_sessions : has
users ||--o{ dashboard_preferences : has
users ||--o{ auto_enrollment_tokens : creates
users ||--o{ alerts : assigned_to
users ||--o{ alerts : resolved_by
users ||--o{ alert_history : creates
hosts ||--o{ host_packages : has
hosts ||--o{ host_repositories : has
hosts ||--o{ host_group_memberships : belongs_to
hosts ||--o{ update_history : has
hosts ||--o{ job_history : has
hosts ||--o{ docker_containers : has
hosts ||--o{ docker_volumes : has
hosts ||--o{ docker_networks : has
hosts ||--o{ compliance_scans : has
packages ||--o{ host_packages : "installed on"
repositories ||--o{ host_repositories : "used by"
host_groups ||--o{ host_group_memberships : contains
host_groups ||--o{ auto_enrollment_tokens : "default group"
compliance_profiles ||--o{ compliance_scans : used_in
compliance_profiles ||--o{ compliance_rules : contains
compliance_scans ||--o{ compliance_results : produces
compliance_rules ||--o{ compliance_results : evaluated_in
docker_images ||--o{ docker_containers : used_by
docker_images ||--o{ docker_image_updates : has
alerts ||--o{ alert_history : has
alert_config ||--o| users : "auto_assign_to"
Key Tables
Core Tables
users
- Authentication (password_hash, tfa_secret)
- OIDC integration (oidc_sub, oidc_provider)
- Preferences (theme, dashboard layout)
- Role-based permissions
hosts
- Unique API credentials (api_id, api_key) - scoped per host
- System information (OS, hardware, network)
- Status tracking (status, last_update)
- Feature flags (docker_enabled, compliance_enabled)
packages
- Package metadata (name, description, category)
- Version tracking (latest_version)
host_packages
- Package instances on hosts
- Update status (needs_update, is_security_update)
- Version tracking (current_version, available_version)
repositories
- Repository definitions (URL, distribution, components)
- Security status (is_secure, is_active)
Docker Tables
docker_images
- Image metadata (repository, tag, digest)
- Source tracking (docker-hub, etc.)
docker_containers
- Container instances per host
- Status and state tracking
docker_image_updates
- Available updates per image
- Security update flags
Compliance Tables
compliance_profiles
- Scan profiles (OpenSCAP, Docker Bench)
- OS family and version mapping
compliance_scans
- Scan execution records
- Results summary (passed, failed, score)
compliance_rules
- Rule definitions per profile
- Severity and remediation info
compliance_results
- Individual rule results per scan
- Findings and remediation details
Alert System Tables
alerts
- Alert records (type, severity, status)
- Assignment and resolution tracking
alert_history
- Audit trail of alert actions
- User actions (created, resolved, silenced)
alert_config
- Per-alert-type configuration
- Auto-assignment rules
- Retention policies
Job Tracking
job_history
- BullMQ job execution records
- Status tracking (active, completed, failed)
- Linked to hosts via api_id
API Credential Scoping
Credential Types
graph TB
subgraph "Credential Types"
HostCreds[Host API Credentials<br/>api_id + api_key<br/>Per-host scoped]
TokenCreds[Auto Enrollment Tokens<br/>token_key + token_secret<br/>Scoped with permissions]
end
subgraph "Host Credentials"
HostCreds -->|Stored in| HostTable["hosts table<br/>api_id: unique<br/>api_key: hashed"]
HostCreds -->|Used by| Agent[Agent Authentication<br/>X-API-ID + X-API-KEY headers]
HostCreds -->|Validated by| HostAuth[validateApiCredentials<br/>middleware]
end
subgraph "Token Credentials"
TokenCreds -->|Stored in| TokenTable["auto_enrollment_tokens table<br/>token_key: unique<br/>token_secret: bcrypt hashed"]
TokenCreds -->|Scopes| Scopes["JSON scopes object<br/>resource: action array"]
TokenCreds -->|IP Restrictions| IPRanges[allowed_ip_ranges<br/>CIDR blocks]
TokenCreds -->|Used by| API[API Integration<br/>Basic Auth]
TokenCreds -->|Validated by| TokenAuth[authenticateApiToken<br/>+ requireApiScope]
end
Authentication Flow
Host Authentication (Agent)
sequenceDiagram
participant Agent
participant Nginx
participant Backend
participant DB
Agent->>Nginx: POST /api/v1/hosts/update<br/>Headers: X-API-ID, X-API-KEY
Nginx->>Backend: Proxy request
Backend->>Backend: validateApiCredentials middleware
Backend->>DB: Find host by api_id
DB-->>Backend: Host record
Backend->>Backend: Verify api_key (bcrypt)
alt Valid credentials
Backend->>Backend: Attach hostRecord to req
Backend->>Backend: Process request
Backend-->>Nginx: 200 OK + Response
Nginx-->>Agent: Response
else Invalid credentials
Backend-->>Nginx: 401 Unauthorized
Nginx-->>Agent: 401 Error
end
Token Authentication (API Integration)
sequenceDiagram
participant Client
participant Nginx
participant Backend
participant DB
Client->>Nginx: POST /api/v1/hosts<br/>Authorization: Basic base64(token_key:token_secret)
Nginx->>Backend: Proxy request
Backend->>Backend: authenticateApiToken middleware
Backend->>DB: Find token by token_key
DB-->>Backend: Token record
Backend->>Backend: Verify token_secret (bcrypt)
Backend->>Backend: Check token.is_active
Backend->>Backend: Check token.expires_at
Backend->>Backend: Check IP restrictions (CIDR)
alt Valid token
Backend->>Backend: requireApiScope middleware
Backend->>Backend: Validate scope permissions
alt Has permission
Backend->>Backend: Attach apiToken to req
Backend->>Backend: Process request
Backend-->>Nginx: 200 OK + Response
Nginx-->>Client: Response
else No permission
Backend-->>Nginx: 403 Forbidden
Nginx-->>Client: 403 Error
end
else Invalid token
Backend-->>Nginx: 401 Unauthorized
Nginx-->>Client: 401 Error
end
Scope Structure
Token scopes are stored as JSON in auto_enrollment_tokens.scopes:
{
"host": ["get", "post", "put", "patch", "delete"],
"package": ["get", "post"],
"compliance": ["get", "post"]
}
Scope Validation:
- Only applies to tokens with
metadata.integration_type === "api" - Validates
scopes[resource].includes(action) - Returns 403 if scope not found
WebSocket Communication
WebSocket Architecture
graph TB
subgraph "Client Layer"
Browser[Web Browser]
Agent[PatchMon Agent]
end
subgraph "Nginx"
NginxProxy[Nginx<br/>WebSocket Upgrade]
end
subgraph "Backend WebSocket Handlers"
AgentWS["Agent WebSocket<br/>/ws/agent"]
SSHWS["SSH Terminal WebSocket<br/>/ws/ssh/:hostId"]
BullWS[Bull Board WebSocket<br/>/bullboard]
end
subgraph "WebSocket Server"
WSS[WebSocket.Server<br/>noServer: true]
end
subgraph "HTTP Server"
HTTPServer[Express HTTP Server]
end
Browser -->|WebSocket Upgrade| NginxProxy
Agent -->|WebSocket Upgrade| NginxProxy
NginxProxy -->|Upgrade Request| HTTPServer
HTTPServer -->|Upgrade Event| WSS
WSS -->|Route by pathname| AgentWS
WSS -->|Route by pathname| SSHWS
WSS -->|Route by pathname| BullWS
Agent WebSocket Flow
sequenceDiagram
participant Agent
participant Nginx
participant Backend
participant WSS
participant DB
Agent->>Nginx: WebSocket Upgrade<br/>/ws/agent?api_id=xxx&api_key=yyy
Nginx->>Backend: Upgrade request
Backend->>Backend: HTTP upgrade event handler
Backend->>DB: Validate api_id + api_key
alt Valid credentials
Backend->>WSS: Accept WebSocket connection
WSS->>Agent: Connection established
Agent->>WSS: Ping every 30s
WSS->>Agent: Pong response
Note over WSS,Agent: Bidirectional communication
WSS->>Agent: Command: report_now
Agent->>WSS: Response: update_data
WSS->>Agent: Command: update_agent
Agent->>WSS: Response: update_status
WSS->>Agent: Command: compliance_scan
Agent->>WSS: Progress: compliance_scan_progress
WSS->>Agent: Command: docker_inventory_refresh
Agent->>WSS: Event: docker_status
else Invalid credentials
Backend->>Agent: Connection rejected
end
SSH Terminal WebSocket Flow
sequenceDiagram
participant Browser
participant Nginx
participant Backend
participant SSHWS
participant AgentWS
participant Agent
participant SSHHost
Browser->>Nginx: WebSocket Upgrade<br/>/ws/ssh/:hostId<br/>Cookie: session_token
Nginx->>Backend: Upgrade request
Backend->>Backend: Authenticate user session
alt Authenticated
Backend->>SSHWS: Accept WebSocket connection
SSHWS->>Browser: Connection established
Browser->>SSHWS: {type: "connect", connection_mode: "proxy"}
SSHWS->>SSHWS: Generate proxy session ID
SSHWS->>AgentWS: Forward to agent WebSocket
AgentWS->>Agent: SSH proxy request
Agent->>SSHHost: SSH connection
Agent->>AgentWS: SSH data stream
AgentWS->>SSHWS: Forward SSH data
SSHWS->>Browser: Display terminal
Browser->>SSHWS: Terminal input
SSHWS->>AgentWS: Forward input
AgentWS->>Agent: Forward to SSH
Agent->>SSHHost: Send input
end
WebSocket Message Types
Agent → Backend
docker_status- Docker container status eventscompliance_scan_progress- Compliance scan progress updatesssh_proxy_data- SSH terminal data (proxy mode)ssh_proxy_connected- SSH connection establishedssh_proxy_error- SSH connection errorssh_proxy_closed- SSH connection closed
Backend → Agent
report_now- Force immediate update reportupdate_agent- Update agent binarysettings_update- Update agent settings (update_interval)refresh_integration_status- Refresh integration statusdocker_inventory_refresh- Refresh Docker inventorycompliance_scan- Start compliance scanremediate_rule- Remediate specific compliance rulessh_proxy_request- SSH proxy connection request
Agent Communication Flow
Agent Registration & Enrollment
sequenceDiagram
participant Agent
participant Nginx
participant Backend
participant DB
Note over Agent: Agent installed on host
Agent->>Nginx: POST /api/v1/hosts/enroll<br/>Authorization: Basic (token_key:token_secret)
Nginx->>Backend: Proxy request
Backend->>Backend: authenticateApiToken middleware
Backend->>DB: Validate enrollment token
Backend->>DB: Check IP restrictions
Backend->>DB: Check max_hosts_per_day
Backend->>DB: Create host record<br/>Generate api_id + api_key
DB-->>Backend: Host created
Backend->>DB: Assign to default host group
Backend-->>Nginx: 201 Created<br/>{api_id, api_key}
Nginx-->>Agent: Credentials
Agent->>Agent: Save credentials to<br/>/etc/patchmon/credentials.yaml
Agent Update Reporting
sequenceDiagram
participant Agent
participant Nginx
participant Backend
participant DB
participant BullMQ
Note over Agent: Scheduled update check<br/>(every update_interval minutes)
Agent->>Nginx: POST /api/v1/hosts/update<br/>Headers: X-API-ID, X-API-KEY<br/>Body: {packages, repositories, ...}
Nginx->>Backend: Proxy request
Backend->>Backend: validateApiCredentials
Backend->>DB: Update host record
Backend->>DB: Upsert packages
Backend->>DB: Upsert repositories
Backend->>DB: Create update_history record
Backend->>BullMQ: Queue system-statistics job
Backend-->>Nginx: 200 OK
Nginx-->>Agent: Response
Agent Command Execution
sequenceDiagram
participant User
participant Frontend
participant Backend
participant BullMQ
participant Worker
participant AgentWS
participant Agent
User->>Frontend: Click "Update Agent" button
Frontend->>Backend: POST /api/v1/hosts/:id/update-agent
Backend->>Backend: Authenticate user (session)
Backend->>Backend: Check permissions
Backend->>BullMQ: Add job to agent-commands queue<br/>{api_id, type: "update_agent"}
Backend-->>Frontend: 202 Accepted
Frontend-->>User: "Update queued"
BullMQ->>Worker: Process job
Worker->>Backend: Check agent WebSocket connection
Worker->>AgentWS: Send command via WebSocket
AgentWS->>Agent: WebSocket message<br/>{type: "update_agent"}
Agent->>Agent: Download new binary
Agent->>Agent: Restart with new binary
Agent->>AgentWS: Update status
AgentWS->>Worker: Command completed
Worker->>DB: Update job_history status
Authentication & Authorization
User Authentication Flow
graph TB
subgraph "Authentication Methods"
LocalAuth[Local Auth<br/>Username + Password]
TFA[Two-Factor Auth<br/>TOTP]
OIDC[OIDC/OAuth2<br/>Optional]
end
subgraph "Session Management"
Session[User Session<br/>httpOnly cookie]
RefreshToken[Refresh Token<br/>Database stored]
AccessToken[Access Token<br/>JWT in cookie]
end
subgraph "Authorization"
Roles[Role-Based<br/>admin, user, viewer]
Permissions[Permission Matrix<br/>role_permissions table]
end
LocalAuth -->|+ TFA| TFA
LocalAuth -->|+ OIDC| OIDC
TFA --> Session
OIDC --> Session
Session --> RefreshToken
RefreshToken --> AccessToken
AccessToken --> Roles
Roles --> Permissions
Permission Matrix
| Permission | Admin | User | Viewer |
|---|---|---|---|
| View Dashboard | ✅ | ✅ | ✅ |
| View Hosts | ✅ | ✅ | ✅ |
| Manage Hosts | ✅ | ❌ | ❌ |
| View Packages | ✅ | ✅ | ✅ |
| Manage Packages | ✅ | ❌ | ❌ |
| View Users | ✅ | ❌ | ❌ |
| Manage Users | ✅ | ❌ | ❌ |
| Manage Superusers | ✅ | ❌ | ❌ |
| View Reports | ✅ | ✅ | ✅ |
| Export Data | ✅ | ❌ | ❌ |
| Manage Settings | ✅ | ❌ | ❌ |
Security Features
-
Password Security
- Bcrypt hashing (10 rounds)
- Password strength requirements
-
Session Security
- HttpOnly cookies (XSS protection)
- Secure flag (HTTPS only)
- SameSite protection (CSRF mitigation)
- Device fingerprinting
- IP address tracking
-
Two-Factor Authentication
- TOTP (Time-based One-Time Password)
- Backup codes
- Remember device option
-
API Security
- Rate limiting
- IP restrictions (CIDR blocks)
- Token expiration
- Scope-based permissions
- Brute force protection
-
WebSocket Security
- Authentication before upgrade
- API credential validation
- Message size limits (64KB)
- Connection timeout handling
Data Flow Examples
Package Update Detection Flow
sequenceDiagram
participant Agent
participant Backend
participant DB
participant BullMQ
participant Worker
participant Frontend
Note over Agent: Agent checks for updates
Agent->>Backend: POST /api/v1/hosts/update<br/>packages: [{name, current_version, available_version}]
Backend->>DB: Upsert packages table
Backend->>DB: Upsert host_packages table<br/>Set needs_update, is_security_update flags
Backend->>DB: Create update_history record
Backend-->>Agent: 200 OK
Backend->>BullMQ: Queue system-statistics job
BullMQ->>Worker: Process job
Worker->>DB: Aggregate statistics
Worker->>DB: Create system_statistics record
Frontend->>Backend: GET /api/v1/hosts/:id/packages
Backend->>DB: Query host_packages with filters
DB-->>Backend: Package list
Backend-->>Frontend: Packages with update status
Compliance Scan Flow
sequenceDiagram
participant User
participant Frontend
participant Backend
participant BullMQ
participant Worker
participant AgentWS
participant Agent
participant OpenSCAP
User->>Frontend: Start compliance scan
Frontend->>Backend: POST /api/v1/compliance/scans/start
Backend->>Backend: Authenticate user
Backend->>BullMQ: Queue agent-commands job<br/>{type: "compliance_scan", profile_id}
Backend-->>Frontend: 202 Accepted
BullMQ->>Worker: Process job
Worker->>AgentWS: Send WebSocket command
AgentWS->>Agent: {type: "compliance_scan", profile_id}
Agent->>OpenSCAP: Run oscap scan
OpenSCAP-->>Agent: Scan results (ARF/XML)
Agent->>AgentWS: Progress updates<br/>{type: "compliance_scan_progress"}
AgentWS->>Backend: Forward progress
Backend->>Frontend: WebSocket broadcast
Agent->>Backend: POST /api/v1/compliance/scans<br/>Scan results
Backend->>Backend: Parse results
Backend->>DB: Create compliance_scans record
Backend->>DB: Create compliance_results records
Backend-->>Agent: 200 OK
Frontend->>Backend: GET /api/v1/compliance/scans/:id
Backend->>DB: Query scan results
DB-->>Backend: Scan details
Backend-->>Frontend: Results with pass/fail status
Component Details
Backend Structure
backend/
├── src/
│ ├── server.js # Main Express server
│ ├── middleware/
│ │ ├── auth.js # User authentication
│ │ ├── apiAuth.js # API token authentication
│ │ └── apiScope.js # API scope validation
│ ├── routes/ # API route handlers
│ ├── services/
│ │ ├── automation/ # BullMQ queue management
│ │ ├── agentWs.js # Agent WebSocket handler
│ │ └── sshTerminalWs.js # SSH terminal WebSocket
│ └── utils/ # Utilities
└── prisma/
└── schema.prisma # Database schema
Frontend Structure
frontend/
├── src/
│ ├── components/ # React components
│ ├── pages/ # Page components
│ ├── hooks/ # Custom React hooks
│ ├── utils/
│ │ └── api.js # API client (axios)
│ └── App.jsx # Main app component
└── dist/ # Built static files
Agent Structure
agent-source-code/
├── cmd/patchmon-agent/
│ └── commands/
│ └── serve.go # WebSocket server
├── internal/
│ ├── client/ # HTTP client
│ ├── config/ # Configuration management
│ ├── integrations/ # Docker, compliance integrations
│ └── packages/ # Package manager interfaces
└── pkg/models/ # Data models
Deployment Architecture
Docker Compose Setup
graph TB
subgraph "Docker Network"
NginxContainer[Nginx Container<br/>Port 3000]
BackendContainer[Backend Container<br/>Port 3001]
FrontendContainer[Frontend Container<br/>Static Files]
PostgresContainer[PostgreSQL Container<br/>Port 5432]
RedisContainer[Redis Container<br/>Port 6379]
end
NginxContainer -->|Proxy| BackendContainer
NginxContainer -->|Serve| FrontendContainer
BackendContainer -->|Connect| PostgresContainer
BackendContainer -->|Connect| RedisContainer
Production Setup
graph TB
subgraph "Server"
Nginx[Nginx<br/>System Service<br/>Port 80/443]
Backend[Node.js Backend<br/>PM2/Systemd<br/>Port 3001]
Frontend[Static Files<br/>/opt/patchmon/frontend/dist]
Postgres[PostgreSQL<br/>System Service<br/>Port 5432]
Redis[Redis<br/>System Service<br/>Port 6379]
end
Internet -->|HTTPS| Nginx
Nginx -->|Proxy| Backend
Nginx -->|Serve| Frontend
Backend -->|Connect| Postgres
Backend -->|Connect| Redis
Performance Considerations
-
Database Connection Pooling
- Prisma connection pool (default: 10 connections)
- Optimized for multiple worker instances
-
Redis Connection Optimization
- Shared connection pool for BullMQ
- Connection reuse across queues and workers
-
WebSocket Connection Management
- Connection tracking by api_id
- Automatic reconnection handling
- Ping/pong keepalive (30s interval)
-
Job Processing
- Low concurrency (1 job/worker) to reduce load
- Exponential backoff for retries
- Job retention limits (50 completed, 20 failed)
-
Caching
- Integration state cache (60s TTL)
- Static asset caching (1 year)
- API response caching where appropriate
Security Architecture
Defense in Depth
-
Network Layer
- Nginx reverse proxy
- SSL/TLS encryption
- Security headers
- Rate limiting
-
Application Layer
- Input validation
- SQL injection prevention (Prisma ORM)
- XSS protection (React escaping)
- CSRF protection (SameSite cookies)
-
Authentication Layer
- Password hashing (bcrypt)
- Token encryption
- Session management
- 2FA support
-
Authorization Layer
- Role-based access control
- API scope validation
- IP restrictions
- Resource-level permissions
-
Data Layer
- Encrypted credentials storage
- Audit logging
- Data retention policies
- Secure credential transmission
Monitoring & Observability
Bull Board
- URL:
/bullboard(secured, requires authentication) - Features:
- Queue status (waiting, active, completed, failed)
- Job details and logs
- Worker status
- Retry management
Health Checks
- Endpoint:
/health - Checks:
- Database connectivity
- Redis connectivity
- Service status
Logging
- Winston logger (configurable)
- Log levels: error, warn, info, debug
- File and console outputs
- Structured JSON logging
Conclusion
This architecture provides:
- Scalability: Queue-based job processing, connection pooling
- Security: Multi-layer security, credential scoping, audit trails
- Reliability: Retry mechanisms, health checks, graceful shutdown
- Maintainability: Clear separation of concerns, modular design
- Observability: Bull Board monitoring, comprehensive logging
For specific implementation details, refer to the source code in the respective directories.
WebSockets Information and Design
PatchMon WebSockets
This document describes how WebSockets are used in PatchMon: endpoints, authentication, WS vs WSS behaviour, and security.
Overview
PatchMon uses a single HTTP server with noServer: true WebSocket handling. All WebSocket upgrades are handled in one place (server.on("upgrade")) and routed by path:
| Path pattern | Purpose | Clients |
|---|---|---|
/api/{v}/agents/ws |
Agent ↔ server persistent connection | PatchMon agent (Go) |
/api/{v}/ssh-terminal/:hostId |
Browser SSH terminal to a host | Frontend (browser) |
/bullboard* |
Bull Board queue UI real-time updates | Bull Board (browser) |
Implementation lives in:
- Backend:
backend/src/services/agentWs.js(agent + Bull Board),backend/src/services/sshTerminalWs.js(SSH terminal).
WebSocket Endpoints
Agent WebSocket
- Path:
/api/{version}/agents/ws(e.g./api/v1/agents/ws) - Auth: HTTP headers on the upgrade request:
X-API-ID,X-API-KEY(validated againsthostsand API key utils). - Purpose: Persistent connection from each agent to the server for:
- Heartbeat / presence
- Commands from server (e.g. trigger update, compliance scan)
- Events from agent (e.g. Docker status)
SSH Terminal WebSocket
- Path:
/api/{version}/ssh-terminal/:hostId(e.g./api/v1/ssh-terminal/abc-123) - Auth: One-time ticket (query
ticket=...) or legacy JWT (token=...orAuthorization: Bearer ...). User must havecan_manage_hosts(or admin). - Purpose: Browser opens a WebSocket to the backend; backend either connects directly to the host via SSH or proxies through the agent WebSocket to the host.
Bull Board WebSocket
- Path:
/bullboard(prefix match) - Auth: Session cookie
bull-board-sessionorAuthorizationheader. - Purpose: Real-time updates for the Bull Board queue UI (echo-style).
WS vs WSS (Secure vs Insecure)
How each part of the system decides between ws (insecure) and wss (secure):
Server (backend)
The server does not choose the protocol; it detects whether the incoming connection is secure and records it for logging and metadata:
socket.encrypted—truewhen the TCP socket is TLS (direct wss to Node).request.headers["x-forwarded-proto"] === "https"—truewhen TLS is terminated at a reverse proxy (e.g. Nginx) that sends the original protocol.
So: if the client connects over TLS, or the proxy sets X-Forwarded-Proto: https, the server treats the connection as secure (wss). Otherwise it is treated as ws.
Code: backend/src/services/agentWs.js (e.g. isSecure, connectionMetadata, log line with protocol=wss|ws).
Agent (Go)
The agent converts the configured server URL to a WebSocket URL and uses that to connect:
| Configured URL | WebSocket URL |
|---|---|
https://... |
wss://... |
http://... |
ws://... |
Already wss:// or ws:// |
Used as-is |
| No protocol | Assumed HTTPS → wss://... |
The path is then appended: .../api/{version}/agents/ws.
Code: agent-source-code/cmd/patchmon-agent/commands/serve.go (connectOnce, URL conversion and wsURL).
Frontend (browser)
The frontend (e.g. SSH terminal) builds the WebSocket URL so it matches the page:
- Page loaded over HTTPS →
wss: - Page loaded over HTTP →
ws:
So the same app works on http and https without extra configuration.
Code: frontend/src/components/SshTerminal.jsx (e.g. protocol = window.location.protocol === "https:" ? "wss:" : "ws:").
Authentication
- Agent WS: Credentials only on the upgrade request via
X-API-IDandX-API-KEY. No auth in query or body. Keys are validated (including bcrypt/legacy) before the WebSocket is accepted. - SSH terminal: Prefer one-time ticket in query (
ticket=...); ticket is consumed once. Legacy:token=...orAuthorization: Bearer .... User must be authorized for the host (e.g.can_manage_hosts). - Bull Board: Session cookie or
Authorizationheader; required before accepting the upgrade.
All three paths reject the upgrade (e.g. 401) if auth fails; the WebSocket is never established.
Message Types and Flows
- Agent → Backend: e.g.
docker_status, pings. JSON over the agent WebSocket. - Backend → Agent: e.g.
update_agent,compliance_scan, SSH proxy payloads. Sent over the same WebSocket by api_id. - SSH terminal: Browser sends
connectwith SSH options; backend (or agent) establishes SSH and forwards terminal I/O over the WebSocket. Resize and other control messages are defined insshTerminalWs.jsand the frontend.
Security Notes
- TLS in production: Use HTTPS and WSS. The agent assumes WSS when the server URL is
https://or has no scheme. - Proxy: When behind Nginx (or similar), ensure
X-Forwarded-Proto: httpsis set for HTTPS so the backend correctly detects secure connections. - Agent TLS verification:
skip_ssl_verifyis blocked whenPATCHMON_ENV=production; avoid disabling TLS verification in production. - SSH terminal: Prefer one-time tickets; avoid putting long-lived tokens in URLs (logs, history). Permissions are enforced per user/role (
can_manage_hosts). - Brute force / abuse: Upgrade is rejected with 401/404 before the WebSocket is created; no WebSocket resource is exposed without valid auth.
General Information
Metrics collection information
We decided it would be useful for us and the community to understand and know three key pieces of information about PatchMon instances out in the field.
- Qty of installations / live setups
- Qty of hosts being monitored
- Version number of your instance
This is so we can produce a metric on the website to show this live statistic.
I consulted with the community on Discord explaining how it's done (the chat can be seen in the Security channel)
Essentially this is the flow:
The main reason is, as a founder it would be amazing to track progression of the app globally to understand how many instances are out there. However something to also note, if there is a security concern with a previous version then we need to know how many instances of that version is out in the field.
Questions:
Do you collect IPs?
Nope - IP addresses are not outputted into any file or seen in a console log when your instance reaches out to us.
How do I opt-in or out ?
Go into settings, metrics and press the toggle button to stop the schedule
How do I delete the information about my qty of hosts and version number you have ?
Please send us an email to support@patchmon.net with your uuid and we will remove it from the database. Note that if you do this then this will be the only time we will be able to associate you with your instance id
What happens if I regenerate my instance id?
In our reports we get a new instance ID and it's duplicated, we have no way of knowing which instance it has replaced however our metrics will look at last 7 days active instances and we will use this metric on the website, so after 7 days the number will drop again
Can I see the code for this?
Absolutely, this project and its code for the app is viewable on github, you'll be able to see how the metrics collector works.
Migrating from 1.4.2 to 1.5.0
This is a migration document for those on 1.4.2 to 1.5.0
I've only tested this from 1.4.2 to 1.5.0 . If you're on a lower version like 1.3.x then i'm not sure this will entirely work but there is a chance it will as the main aspect is the database migration.
Usually there are three types of deployments:
- via docker-compose.yml
- via our native setup.sh script
- via proxmox community scripts
docker-compose.yml is the preferred and supported way.
We had setup.sh originally when we first started the project, at that time we didn't have docker images and we have carried on supporting it until now.
The issue with installing PatchMon natively on an OS is that there are evolving changes as we have progressed and keeping on top of them (also via edge cases) becomes a very heavy task.
Shipping containers is much easier and also more secure as now we a eusing hardened images from docker to really reduce the CVEs that images are shipped with.
Technically there is a way still to not use docker, I will talk about it but I can't support it officially. I can only point to the right direction.
The proxmox-community scripts team are a great bunch, I am yet to speak to them about these major changes and work with them so that upgrade / installation is done seamlessly.
For now I will document the process for
- Upgrading via docker
- Conversion of native setup.sh type to docker
The main thing when it comes to migrations is carrying over the database and ensuring that the new .env has the necessary entries.
The new 1.5.0 brings new features like Patching but more importantly has been completely re-written in GO LANG . Both agent and server are now in GO.
We have seen memory footprint of PatchMon server go from 500MB to 50MB and execution times become much faster.
Docker migration from 1.4.2 to 1.5.0
Key changes made in new version :
- Single container for "patchmon-server" as opposed to "backend" and "frontend" . The frontend built static files are now embedded in the backend go binary file served via the container
- Agents volume is now not needed - binaries are now embedded in the container image as opposed to a writable volume.
- Branding assets volume also now not needed - images are now stored in the database as opposed to a writable assets volume
- Fully utilised the .env file for all variables and is shared between the various containers - this keeps the docker-compose.yml clean and we only need to work on looking at the .env file for any variables.
- Utilising docker hardened images upon build for additional security.
Easy script migration for new docker-compose.yml and .env
- Go to your docker directory where you have the docker-compose.yml and .env file
- Run the following :
curl -fsSL https://raw.githubusercontent.com/PatchMon/PatchMon/refs/heads/1-5-0-post-jobs/tools/migrate1-4-2_to_1-5-0.sh | bash
This will download the new docker-compose.yml and .env whilst migrating your variables to it. Please review your new .env before performing docker compose up.