Skip to main content
PathMon

Chapter 6 of 7

Auto-Enrollment API Documentation

Updated Read the full guide

Overview

PatchMon's auto-enrollment API enables automated device onboarding using tools like Ansible, Terraform, or custom scripts. It covers token management, host enrollment, and agent installation endpoints.

Table of Contents

API Architecture

Base URL Structure

https://your-patchmon-server.com/api/v1/

The API version is v1 and is fixed in the server.

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-Secret headers
  • 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-KEY headers
  • Rate Limited: No (per-host)
  • Storage: API key is hashed (bcrypt) in the database

Why two tiers?

  • Compromised enrollment token does not compromise existing hosts
  • Compromised host credential does not compromise 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 (required) 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_secret is 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 exist
  • 400 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.

Both scripts can be run again on a host that is already enrolled. They skip a host whose agent works, reuse its credentials if PatchMon still accepts them, and otherwise enrol with the host's host_id so that a host which never reported gets new credentials rather than a duplicate record. See Running the Script Again in the Proxmox chapter; the direct-host script behaves the same way for the machine it runs on. On a machine with no usable /etc/machine-id or /var/lib/dbus/machine-id (most containers), the direct-host and install scripts generate a machine ID once and keep it in /var/lib/patchmon/machine-id, so every later run sends the same one; the removal script leaves that file in place, as it does /etc/machine-id. Existing credentials and host_id are only used when the agent's config.yml points at the same PatchMon URL the script was downloaded from, so after a change of server URL repair the host with the install command from its host page. To create a new host for this machine even when its agent works (for example on a cloned machine), set FORCE_NEW_HOST=true. The script moves /etc/patchmon/config.yml, credentials.yml and their backups into /etc/patchmon/pre-force-new-<timestamp>/ first, so they are never reused, and puts them back if no new host is created. If PatchMon answers that a host with this machine ID and name already exists, that host is the original (or one an earlier FORCE_NEW_HOST run created for this machine). Do not remove it; rerun with a different FRIENDLY_NAME, or give the clone its own /etc/machine-id (or, if it has none, delete its copied /var/lib/patchmon/machine-id):

curl -s "https://patchmon.example.com/api/v1/auto-enrollment/script?type=direct-host&token_key=KEY&token_secret=SECRET" | FORCE_NEW_HOST=true sh

Errors:

  • 400 Bad Request: Missing or invalid type parameter
  • 401 Unauthorized: Missing credentials, invalid/inactive token, invalid secret, or expired token
  • 404 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)
host_id string No The host_id from the machine's existing config.yml, if it has one (1 to 64 letters, digits or hyphens). Lets PatchMon recognise a machine it has already enrolled. Never used as the ID of a new host. Added in 2.1.4
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_key is only returned in this response (plain text). It is hashed before storage. The host_group is null if no default host group is configured on the token.

Response: 200 OK (credentials re-issued)

Returned when host_id names a host that exists, has never reported or connected, was created by this same token, and was enrolled with the same machine_id as this request. PatchMon issues a new API key for that host and returns the same body as 201, with "message": "Host credentials re-issued" and "reenrolled": true. The host keeps its ID and its API ID; the old key stops working. Nothing new is created, so the token's daily host limit is neither checked nor consumed.

{
  "message": "Host credentials re-issued",
  "host": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "friendly_name": "webserver",
    "api_id": "patchmon_abc123def456",
    "api_key": "new-raw-api-key-value",
    "host_group": null,
    "status": "pending"
  },
  "reenrolled": true
}

How host_id is resolved:

Host named by host_id Result
Not found Enrolled as a new host (201), exactly as if host_id were not sent
Has reported, or its agent has connected to PatchMon 409, code host_reporting. Such a host is never re-keyed by enrolment
Never reported, created by a different token, in the UI, or before 2.1.4 409, code host_not_reenrollable
Never reported, created by this token, but enrolled with a different machine_id 409, code host_not_reenrollable
Never reported, created by this token, credentials re-issued less than 60 seconds ago 429, code reenrol_cooldown
Never reported, created by this token 200, credentials re-issued

Responses from this path carry code and host_id alongside error, plus friendly_name when the host was created by this token:

{
  "error": "This host is already reporting to PatchMon. If its agent is broken, reinstall it with the install command on the host's page, or regenerate its credentials.",
  "code": "host_reporting",
  "friendly_name": "webserver",
  "host_id": "550e8400-e29b-41d4-a716-446655440000"
}

Error Responses:

Status Error Cause
400 Validation errors Missing or invalid friendly_name
400 Invalid host_id host_id is not 1 to 64 letters, digits or hyphens
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
409 Host already enrolled Code machine_id_match. No host_id was sent and a host with the same machine_id and name already exists; the body also carries that host's friendly_name
409 (message) Code host_reporting or host_not_reenrollable; see the table above
429 Rate limit exceeded Token's max_hosts_per_day limit reached on a request that would create a host
429 (message) Code reenrol_cooldown. Credentials for the host named by host_id were re-issued less than 60 seconds ago
429 Too many requests. Try again later. Per-IP agent rate limit hit (AGENT_RATE_LIMIT_MAX per AGENT_RATE_LIMIT_WINDOW_MS, 1000 per minute by default); the body uses a message key and a Retry-After header is set

Duplicate handling: When machine_id is supplied, the server looks for an existing host with the same machine ID and the same name (compared case-insensitively) and returns 409 Conflict instead of creating a second record. Matching on both means cloned containers that share one /etc/machine-id but carry different names still enrol as separate hosts. No credentials are generated for a rejected request. Requests without a machine_id are never deduplicated. The enrolment scripts treat a 409 as an already enrolled host and move on. This check applies only when no host_id is sent, or when the host_id is not found.

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. Inside a container (Proxmox LXC, Docker) this is the container's own uptime, not the host's
bootTime string No Boot instant as a UTC ISO 8601 timestamp. Lets the UI tick uptime live between reports
loadAverage array No Load average values
machineId string No Machine ID
hostId string No host_id from the agent's config.yml (agent 2.1.4+). A value that differs from the authenticated host's ID does not reject the report; the host is flagged with a host ID mismatch
configVersion string No config_version from the agent's config.yml (agent 2.1.4+). Recorded only when it starts with N.N.N (optionally followed by a pre-release or build suffix) and is at most 64 bytes
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.5.0",
  "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,
  "hostId": "550e8400-e29b-41d4-a716-446655440000"
}

hostId (2.1.4+) is the ID of the host the credentials belong to. Agents record it in config.yml as host_id when they have none.

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 }}"

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, and credentials re-issued by enrolment (reenrolled: true)
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
409 Conflict Enrolment refused; see the code field
429 Too Many Requests Token's daily host creation limit exceeded, or enrolment re-issue cooldown (reenrol_cooldown)
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 hosts per day allowed for this token"
}

Error with code (enrolment 409 and 429 responses):

{
  "error": "Credentials for this host were re-issued less than a minute ago. Try again shortly.",
  "code": "reenrol_cooldown",
  "friendly_name": "webserver",
  "host_id": "550e8400-e29b-41d4-a716-446655440000"
}

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

The limit counts hosts created. Re-issuing credentials for an existing host (200, reenrolled: true) is not counted and is not blocked by the limit.

When the limit is exceeded, the API returns 429 Too Many Requests:

{
  "error": "Rate limit exceeded",
  "message": "Maximum hosts per day allowed for this token"
}

Re-issue Cooldown

PatchMon re-issues credentials for a given host at most once every 60 seconds. A second request within that window gets 429 Too Many Requests with "code": "reenrol_cooldown". This stops two copies of a cloned machine from repeatedly taking a pending host's credentials from each other.

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 create hosts, and issue a new API key for a host created by the same token that has never reported. They cannot re-key a host that has reported, and 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_signed server 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_at timestamp updated on each enrollment
  • Credential re-issues and refused re-enrolments are written to the server log with the host ID, token name and client IP, and the time of the latest re-issue is shown on the host's page

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 a host

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
# 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