Version: 1.0.0 Status: Canonical Location: Linux Patch Manager repo (canonical source) Agent reference: The Agent repo's AGENTS.md references this document as the authoritative contract.
This document defines the interface contract between the Linux Patch Manager (management plane) and the Linux Patch API (agent on managed hosts). Both repos must conform to this contract. Changes require updating this document first, then each repo's implementation.
All agent endpoints use base path /api/v1/, port 12443, TLS 1.3, mTLS authentication.
| Method | Path | Sync/Async | Purpose |
|---|---|---|---|
| GET | /packages |
Sync | List installed packages (filters: name, status, upgradable, sort, order) |
| GET | /packages/{name} |
Sync | Get specific package details |
| POST | /packages |
Async (202) | Install package(s) with optional version pinning |
| PUT | /packages/{name} |
Async (202) | Update specific package |
| DELETE | /packages/{name} |
Async (202) | Remove package |
| Method | Path | Sync/Async | Purpose |
|---|---|---|---|
| GET | /patches |
Sync | List available updates/patches |
| POST | /patches/apply |
Async (202) | Apply all or specific patches (optional reboot) |
| Method | Path | Sync/Async | Purpose |
|---|---|---|---|
| GET | /system/info |
Sync | OS version, kernel, architecture, last update, pending reboot |
| GET | /health |
Sync | Agent health (status, uptime, version, CRL status, GPG key status) |
| POST | /system/reboot |
Async (202) | Reboot host (optional delay, force flag) |
| POST | /system/update |
Async (202) | Trigger agent self-update from manager-hosted repo |
| GET | /system/update/status |
Sync | Get most recent self-update result |
| Method | Path | Sync/Async | Purpose |
|---|---|---|---|
| GET | /pki/repo-config |
Sync | Fallback fetch of repo config (for agents enrolled before repo provisioning) |
| Method | Path | Sync/Async | Purpose |
|---|---|---|---|
| GET | /jobs |
Sync | List all jobs (optional status filter, limit) |
| GET | /jobs/{id} |
Sync | Get specific job status (progress, logs) |
| POST | /jobs/{id}/rollback |
Async (202) | Rollback a completed/failed job (exclusive mode) |
| WS | /ws/jobs |
WebSocket | Real-time job status streaming (subscribe by job_id or all) |
All agent responses use:
{
"success": boolean,
"request_id": "UUID",
"timestamp": "ISO 8601",
"data": object | null,
"error": { "code": string, "message": string, "details": object, "retryable": boolean } | null
}Endpoint: POST /api/v1/enroll (unauthenticated)
Request payload:
{
"machine_id": "string (from /etc/machine-id)",
"fqdn": "string",
"ip_address": "string (non-loopback IPv4)",
"os_details": {
"distro": "string",
"version": "string",
"id_like": "string",
"codename": "string",
"kernel": "string"
}
}Response (202):
{
"polling_token": "string"
}Rate limit: 1 request/minute per IP (HTTP 429 on violation)
Endpoint: GET /api/v1/enroll/status/{token} (unauthenticated)
| Status | HTTP | Response |
|---|---|---|
| Pending | 202 | Empty body |
| Approved | 200 | PkiBundle (see §2.3) |
| Denied | 403 | Error with ENROLLMENT_DENIED |
| Expired/Purged | 404 | Error with ENROLLMENT_EXPIRED |
Polling constraints:
- Default interval: 60 seconds (configurable)
- Hard timeout: 24 hours (1440 attempts max)
- Polling token persisted to agent config.yaml for resume after restart
- Token is single-retrieval: bundle is atomically removed from manager cache on fetch
- Bundle TTL: 10 minutes after approval
This is the canonical structure. Both repos' code already matches this.
{
"ca_crt": "PEM string — leaf-most CA certificate",
"ca_chain": "PEM string — full CA chain (intermediates + root, concatenated). For root mode, same as ca_crt",
"server_crt": "PEM string — agent server certificate",
"server_key": "PEM string — agent server private key (PKCS#8)",
"crl_pem": "PEM string — CRL signed by CA. Empty string if CRL generation failed (agent falls back to degraded mode)",
"repo_config": null or {
"gpg_public_key": "ASCII-armored GPG public key",
"sources_config": "Distro-specific repo config text (apt sources.list line, dnf .repo file, apk URL, pacman include)",
"distro_id": "Distro identifier WITHOUT version (e.g., \"ubuntu\", \"debian\", \"fedora\", \"alpine\", \"arch\")",
"keyring_path": "Filesystem path for GPG key (e.g., \"/etc/apt/keyrings/lpa-repo.gpg\")"
}
}⚠ distro_id format: The canonical format is bare distro name, no version suffix (e.g., "ubuntu", not "ubuntu-24.04"). The Agent's provision_repo_config() at src/enroll/provision.rs:237 matches distro_id against exact bare strings. A version-suffixed value misses every arm and bails. The Manager's detect_distro_id() has been corrected to emit bare ids.
Canonical supported distros (Manager-provisioned): ubuntu, debian, fedora, almalinux, alpine, arch. These are the distros the Manager's detect_distro_id() and generate_distro_config() can handle.
Agent has additional match arms for rhel, centos, rocky, and manjaro at provision.rs:239-261 that the Manager does NOT provision. These arms exist in the agent for forward compatibility but the Manager cannot currently detect or generate repo config for these distros. An unrecognized distro at enrollment MUST fail loudly (explicit error/log), not silently return None.
Current Manager gap: Enrollment approval handler (crates/pm-web/src/routes/enrollment.rs line 322) sets repo_config: None. The struct and types exist, but the handler does not populate repo_config during approval. This must be implemented.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/admin/enrollments |
Admin | List pending enrollment requests |
| POST | /api/v1/admin/enrollments/{id}/approve |
Admin | Approve, generate PKI, migrate to hosts table |
| DELETE | /api/v1/admin/enrollments/{id}/deny |
Admin | Deny and purge request |
| Code | HTTP | Description |
|---|---|---|
ENROLLMENT_DENIED |
403 | Admin rejected enrollment request |
ENROLLMENT_EXPIRED |
404 | Polling token expired or purged |
ENROLLMENT_TIMEOUT |
— | 24-hour polling limit exceeded (agent-side) |
ENROLLMENT_RATE_LIMITED |
429 | Rate limit exceeded (1/minute per IP) |
PKI_PROVISION_FAILED |
— | Certificate write or PEM validation failed (agent-side) |
Per actual code (src/enroll/provision.rs in the Agent), the canonical cert file paths are:
| File | Path | Permissions | Format |
|---|---|---|---|
| CA certificate | /etc/linux_patch_api/certs/ca.pem |
0644 | PEM (X.509) |
| Server certificate | /etc/linux_patch_api/certs/server.pem |
0644 | PEM (X.509) |
| Server private key | /etc/linux_patch_api/certs/server.key.pem |
0600 | PEM (PKCS#8) |
| CRL | /etc/linux_patch_api/certs/crl.pem |
0644 | PEM (CRL) |
Note: The server key file is server.key.pem (with .pem suffix), NOT server.key. This matches the Agent's DEPLOYMENT_GUIDE.md, README.md, configs/CA_SETUP.md, and src/enroll/provision.rs code. The Agent's SPEC.md and ARCHITECTURE.md say server.key — those docs are wrong and must be corrected.
The PkiBundle JSON fields use logical names (ca_crt, server_crt, server_key) — these are NOT file names. The agent writes them to the paths above.
- Port: 80 (plain HTTP)
- Scheme: HTTP (no TLS — GPG signatures provide integrity)
- Base URL:
http://<manager-host>/ - Repo paths:
/apt/,/dnf/,/apk/,/pacman/ - Integrity: Repo metadata signed by the Manager's GPG key
- GPG key: Per-manager, stored alongside CA in
/etc/patch-manager/ca/ - GPG key delivery: Via enrollment bundle
repo_config.gpg_public_keyor fallbackGET /api/v1/pki/repo-config
Self-updating the agent is a standard package update — no different from updating any other package. The Manager uses the existing package update endpoint:
Endpoint: PUT /api/v1/packages/linux-patch-api (Manager calls Agent)
This is the standard package update endpoint already defined in §1.1. No custom self-update endpoint is needed.
The agent's maintainer scripts handle the upgrade lifecycle:
- prerm/pre-deinstall: Does NOT stop the service on upgrade — only on removal
- dpkg/apk/pacman: Replaces files on disk while the service keeps running on the old binary in memory
- postinst/post-install: Schedules a 300s delayed service restart:
- systemd distros:
systemd-run --on-active=300s systemctl restart linux-patch-api.service - Alpine (OpenRC):
nohup sh -c 'sleep 300 && rc-service linux-patch-api restart' >/dev/null 2>&1 &
- systemd distros:
- After 300s, the service restarts and loads the new binary
No custom scripts (self-update.sh), detached systemd units, marker files, health check loops, or auto-rollback are needed. The native package manager and standard maintainer scripts handle everything.
Endpoint: GET /api/v1/pki/repo-config (Agent calls Manager, mTLS authenticated)
Response (200):
{
"success": true,
"request_id": "UUID",
"timestamp": "ISO 8601",
"data": {
"repo_config": {
"gpg_public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----",
"sources_config": "deb [signed-by=/etc/apt/keyrings/lpa-repo.gpg] http://patch-manager.example.com/apt u2404 main",
"distro_id": "ubuntu",
"keyring_path": "/etc/apt/keyrings/lpa-repo.gpg"
}
}
},Note: sources_config uses http:// (port 80), NOT https://.
Error (404): PKI_REPO_CONFIG_UNAVAILABLE — Manager has not provisioned repo config for this host's distro.
The following endpoints are deprecated and should not be used for new implementations:
POST /api/v1/system/update— replaced by standardPUT /api/v1/packages/{name}GET /api/v1/system/update/status— replaced by standardGET /api/v1/jobs/{id}
These endpoints may still exist in the codebase for backward compatibility but should be removed in a future cleanup pass.
Endpoint: GET /api/v1/health (Manager calls Agent)
Response (200 — Healthy):
{
"success": true,
"request_id": "UUID",
"timestamp": "ISO 8601",
"data": {
"status": "healthy",
"uptime_seconds": 12345,
"version": "1.5.6-1",
"crl_status": "valid",
"crl_age_seconds": 3600,
"crl_next_update": "2026-07-01T00:00:00Z",
"gpg_key_status": "valid",
"gpg_key_expires_at": "2028-06-27T00:00:00Z"
}
}| Value | Meaning | Manager Health Impact |
|---|---|---|
valid |
CRL present and not expired | Natural status (no override) |
expired |
CRL present but past next_update | degraded if natural status is healthy |
missing |
CRL file not found | degraded if host registered > 24h ago; natural if ≤ 24h |
invalid |
CRL fails to parse or signature verification fails | unreachable (security event) |
degraded |
CRL loaded but verification in degraded mode | Natural status |
null |
Agent doesn't report CRL (older agent) | Natural status |
| Value | Meaning |
|---|---|
valid |
GPG key present and not expired |
expired |
GPG key past expiration date |
missing |
GPG key not found (agent enrolled before repo feature) |
revoked |
GPG key has been revoked |
The Manager has its own liveness endpoint at GET /status/health (unauthenticated). This is distinct from the Agent's GET /api/v1/health. Do not confuse the two.
These are the Manager's own API for admin enrollment management. Documented here because they produce the PkiBundle that the Agent consumes.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/admin/enrollments |
Admin JWT | List pending enrollment requests |
| POST | /api/v1/admin/enrollments/{id}/approve |
Admin JWT | Approve → generate PKI → migrate to hosts |
| DELETE | /api/v1/admin/enrollments/{id}/deny |
Admin JWT | Deny and purge request |
These gaps require code changes. Each carries a file:line citation from current master code.
| Gap | Side | Description | Evidence (file:line) | Status |
|---|---|---|---|---|
| G-01 | Manager | Enrollment handler did not populate repo_config in PkiBundle |
crates/pm-web/src/routes/enrollment.rs:318-383 |
CLOSED — handler now calls detect_distro_id + generate_distro_config + reads GPG key |
| G-07 | Manager | Manager SPEC/ARCHITECTURE/REQUIREMENTS do not mention self-update or package repo feature | SPEC.md (self-update section added), ARCHITECTURE.md:577-579 (endpoints added) |
CLOSED by PR #123 |
| G-08 | Manager | Manager ARCHITECTURE §12.1 was missing 3 self-update endpoints | ARCHITECTURE.md:577-579 (now present) |
CLOSED by PR #123 |
| G-09 | Agent | Agent SPEC.md said server.key but code uses server.key.pem |
src/enroll/provision.rs:18 defines DEFAULT_SERVER_KEY = "/etc/linux_patch_api/certs/server.key.pem" |
CLOSED by PR #116 |
| G-10 | Agent | Agent ARCHITECTURE.md said server.key but code uses server.key.pem |
Same evidence as G-09 | CLOSED by PR #116 |
| G-11 | Both | API repo version numbers inconsistent (SPEC=2.0.0, README=1.0.0, health example=0.0.1) | SPEC.md:6 says v2.0.0, README.md:4 says v1.0.0, API_SPEC.md:362 shows 0.0.1 |
OPEN — needs version reconciliation |
| G-12 | Manager | Manager README port confusion (config said 443, access instructions said 8080) | README.md:158 (was 8080, now 443) |
CLOSED by PR #123 |
| G-13 | Manager | Manager SPEC (0.0.2) behind SDD (0.0.3) | SPEC.md:25 (now 0.0.3), ARCHITECTURE.md:8 (0.0.3) |
CLOSED by PR #123 |
These gaps are deployment tasks on the manager host. They belong in a deployment runbook, not a code-gap list. The code that would consume these resources is already implemented and working.
| Gap | Side | Description | Evidence (code that consumes the resource) | Status |
|---|---|---|---|---|
| G-04 | Manager host | Package repo directory /var/www/lpa-repo/ not created on manager host |
crates/pm-web/src/lib.rs:261 reads state.config.repo.dir; ServeDir serves from it at runtime |
DEPLOYMENT TASK — code is ready, directory must be created and populated |
| G-05 | Manager host | GPG signing key not generated on manager host | crates/pm-web/src/routes/pki.rs:102 reads from repo_config.gpg_public_key_path; crates/pm-core/src/config.rs:91 defines the path config |
DEPLOYMENT TASK — code is ready, key must be generated and placed |
| Gap | Claimed Status | Verified Status | Evidence |
|---|---|---|---|
| G-02 | "may not be fully implemented" | CLOSED — fully implemented | crates/pm-web/src/routes/pki.rs:25 route registered, :83-150 handler reads GPG key, generates distro config, returns RepoConfig |
| G-03 | "distro_id format mismatch" | FIXED — was a real mismatch | Manager models.rs:233 returned version-suffixed; Agent provision.rs:237 expects bare exact match. Fixed: detect_distro_id now returns bare ids, generate_distro_config uses == |
| G-06 | "ServeDir not configured" | CLOSED — fully configured | crates/pm-web/src/lib.rs:260-267 build_repo_router with 4 nest_service entries; crates/pm-web/src/main.rs:161-182 spawns on configured port 80 |
When either repo changes an interface element:
- Update this document first
- Update the implementing repo's code
- Update the consuming repo's code if needed
- Both repos' AGENTS.md files reference this document as the authoritative contract
End of contract — v1.0.0 — 2026-06-29