# 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](#api-architecture)
- [Authentication](#authentication)
- [Admin Endpoints](#admin-endpoints)
- [Enrollment Endpoints](#enrollment-endpoints)
- [Host Management Endpoints](#host-management-endpoints)
- [Ansible Integration Examples](#ansible-integration-examples)
- [Error Handling](#error-handling)
- [Rate Limiting](#rate-limiting)
- [Security Considerations](#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-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 ≠ 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:

```bash
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:

```bash
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:

```bash
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:**
```json
{
  "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`
```json
{
  "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`
```json
[
  {
    "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:**
```json
{
  "is_active": false,
  "max_hosts_per_day": 200,
  "allowed_ip_ranges": ["192.168.1.0/24"]
}
```

**Response:** `200 OK`
```json
{
  "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`
```json
{
  "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:**
```bash
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 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) |
| `metadata` | object | No | Additional metadata (vmid, proxmox_node, ip_address, os_info, etc.) |

**Example Request:**
```json
{
  "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`
```json
{
  "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.

**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:**
```json
{
  "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_id` is optional

**Response:** `201 Created`
```json
{
  "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`):**
```json
{
  "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:**
```json
{
  "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`
```json
{
  "message": "Host updated successfully",
  "packagesProcessed": 1,
  "updatesAvailable": 1,
  "securityUpdates": 0
}
```

## Ansible Integration Examples

### Basic Playbook for Proxmox Enrollment

```yaml
---
- 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

```yaml
---
- 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

```yaml
---
- 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

```yaml
# 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

```yaml
# 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

```yaml
---
- 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:**
```json
{
  "error": "Error message describing what went wrong"
}
```

**Error with detail:**
```json
{
  "error": "Rate limit exceeded",
  "message": "Maximum 100 hosts per day allowed for this token"
}
```

**Validation errors (400):**
```json
{
  "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`:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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

## 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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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)**
```bash
# 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)**
```bash
# 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)**
```bash
# 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
```