Harden production workflows and agent admission
This commit is contained in:
384
api/openapi.yaml
384
api/openapi.yaml
@@ -5,11 +5,16 @@ info:
|
||||
description: |
|
||||
Monlet v1 API contract. Stage 1 frozen.
|
||||
|
||||
All endpoints under `/api/v1`. Authentication uses a single shared Bearer token (see ADR-0007). Agent-facing ingestion endpoints (`/heartbeat`, `/events`) and read-only UI-facing endpoints share the same security scheme in v1.
|
||||
All endpoints under `/api/v1`. Authentication is split (ADR-0007):
|
||||
|
||||
- `agentToken` — self-generated agent Bearer key stored locally by the agent. Used by ingestion paths `POST /api/v1/heartbeat` and `POST /api/v1/events`.
|
||||
- `uiToken` — operator/UI shared Bearer token from `MONLET_AUTH_TOKEN`. Used by every other authenticated endpoint. Supports overlap rotation via space/comma-separated list (PH-008).
|
||||
|
||||
Both are presented as `Authorization: Bearer <token>`. UI token comparison is constant-time. Agent keys are stored server-side only as hashes/fingerprints and never appear in logs, metrics, or notifications.
|
||||
servers:
|
||||
- url: http://127.0.0.1:8000
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- uiToken: []
|
||||
paths:
|
||||
/api/v1/health:
|
||||
get:
|
||||
@@ -45,6 +50,8 @@ paths:
|
||||
post:
|
||||
summary: Ingest agent heartbeat
|
||||
operationId: ingestHeartbeat
|
||||
security:
|
||||
- agentToken: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -62,14 +69,26 @@ paths:
|
||||
$ref: "#/components/responses/ValidationError"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
description: Agent key is blocked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"409":
|
||||
$ref: "#/components/responses/Conflict"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"429":
|
||||
$ref: "#/components/responses/RateLimited"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/events:
|
||||
post:
|
||||
summary: Ingest agent check result events
|
||||
operationId: ingestEvents
|
||||
security:
|
||||
- agentToken: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -87,6 +106,12 @@ paths:
|
||||
$ref: "#/components/responses/ValidationError"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
description: Agent key is blocked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"500":
|
||||
@@ -97,6 +122,7 @@ paths:
|
||||
operationId: listAgents
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Back"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
@@ -115,6 +141,109 @@ paths:
|
||||
$ref: "#/components/schemas/Agent"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/admission/blacklist:
|
||||
get:
|
||||
summary: List blocked agent keys
|
||||
operationId: listAgentBlacklist
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: Agent blacklist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/PageEnvelope"
|
||||
- type: object
|
||||
required: [items]
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AgentBlacklistItem"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
delete:
|
||||
summary: Clear blocked agent keys
|
||||
operationId: clearAgentBlacklist
|
||||
responses:
|
||||
"200":
|
||||
description: Number of removed blacklist rows.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/admission/blacklist/{key_id}:
|
||||
delete:
|
||||
summary: Remove one blocked agent key
|
||||
operationId: deleteAgentBlacklistItem
|
||||
parameters:
|
||||
- name: key_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 64
|
||||
maxLength: 64
|
||||
responses:
|
||||
"200":
|
||||
description: Number of removed blacklist rows.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/admission/accept-all:
|
||||
post:
|
||||
summary: Accept all pending agents
|
||||
operationId: acceptAllPendingAgents
|
||||
parameters:
|
||||
- name: include_rotation
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
description: When false, accepted agents with pending replacement keys are skipped.
|
||||
responses:
|
||||
"200":
|
||||
description: Number of accepted agents.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/admission/block-all:
|
||||
post:
|
||||
summary: Block all pending agents
|
||||
operationId: blockAllPendingAgents
|
||||
responses:
|
||||
"200":
|
||||
description: Number of blocked agent keys.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/admission/remove-all:
|
||||
post:
|
||||
summary: Remove all pending agents
|
||||
operationId: removeAllPendingAgents
|
||||
responses:
|
||||
"200":
|
||||
description: Number of removed agents.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/agents/{agent_id}:
|
||||
get:
|
||||
summary: Get agent detail
|
||||
@@ -132,6 +261,56 @@ paths:
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
delete:
|
||||
summary: Remove agent and all owned state
|
||||
operationId: deleteAgent
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/AgentId"
|
||||
responses:
|
||||
"200":
|
||||
description: Number of removed agents.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
/api/v1/agents/{agent_id}/accept:
|
||||
post:
|
||||
summary: Accept pending agent key
|
||||
operationId: acceptAgent
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/AgentId"
|
||||
responses:
|
||||
"200":
|
||||
description: Number of accepted agents.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
/api/v1/agents/{agent_id}/block:
|
||||
post:
|
||||
summary: Block pending agent key
|
||||
operationId: blockAgent
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/AgentId"
|
||||
responses:
|
||||
"200":
|
||||
description: Number of blocked agent keys.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ActionCountResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
/api/v1/checks:
|
||||
get:
|
||||
summary: List current check states
|
||||
@@ -145,6 +324,7 @@ paths:
|
||||
maxLength: 128
|
||||
pattern: "^[A-Za-z0-9._:-]+$"
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Back"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
@@ -174,7 +354,37 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
enum: [open, resolved]
|
||||
- name: severity
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [warning, critical, unknown]
|
||||
- name: agent_id
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
pattern: "^[A-Za-z0-9._:-]+$"
|
||||
- name: check_id
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 128
|
||||
pattern: "^[A-Za-z0-9._:-]+$"
|
||||
- name: sort
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [status, opened_at, resolved_at]
|
||||
default: status
|
||||
- name: direction
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Back"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
@@ -213,6 +423,7 @@ paths:
|
||||
maxLength: 128
|
||||
pattern: "^[A-Za-z0-9._:-]+$"
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Back"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
@@ -231,12 +442,56 @@ paths:
|
||||
$ref: "#/components/schemas/StoredCheckResultEvent"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/system-summary:
|
||||
get:
|
||||
summary: System navigation summary
|
||||
operationId: getSystemSummary
|
||||
description: |
|
||||
Bounded aggregate counts for the top navigation. Safe to poll from many
|
||||
open dashboards because response size is independent of row counts.
|
||||
responses:
|
||||
"200":
|
||||
description: System summary snapshot.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SystemSummaryResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/v1/notifiers/outbox:
|
||||
get:
|
||||
summary: List notification outbox items
|
||||
operationId: listOutbox
|
||||
parameters:
|
||||
- name: state
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [pending, sending, sent, retry, failed, discarded]
|
||||
- name: notifier
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [debug, telegram, webhook, alertmanager]
|
||||
- name: event_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [firing, resolved]
|
||||
- name: sort
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [created_at, updated_at]
|
||||
default: updated_at
|
||||
- name: direction
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Back"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
@@ -257,10 +512,21 @@ paths:
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
agentToken:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Shared static Bearer token in v1. See ADR-0007.
|
||||
description: |
|
||||
Self-generated agent Bearer key. The agent creates and persists it in
|
||||
local state; the server stores only hash/fingerprint and requires UI
|
||||
acceptance before events affect state. Used only by `/heartbeat` and
|
||||
`/events`. See ADR-0007.
|
||||
uiToken:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: |
|
||||
Operator/UI Bearer token from `MONLET_AUTH_TOKEN`. Multiple tokens may
|
||||
be active simultaneously (space/comma separated) for zero-loss rotation.
|
||||
See PH-008 and ADR-0007.
|
||||
parameters:
|
||||
AgentId:
|
||||
name: agent_id
|
||||
@@ -274,10 +540,18 @@ components:
|
||||
name: cursor
|
||||
in: query
|
||||
required: false
|
||||
description: Opaque pagination cursor returned in `next_cursor`.
|
||||
description: Opaque pagination cursor returned in `next_cursor` (or `prev_cursor` when `back=true`).
|
||||
schema:
|
||||
type: string
|
||||
maxLength: 512
|
||||
Back:
|
||||
name: back
|
||||
in: query
|
||||
required: false
|
||||
description: When true, treat the `cursor` as a `prev_cursor` and return the previous page.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
@@ -300,12 +574,24 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
Conflict:
|
||||
description: Request conflicts with current server state.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
PayloadTooLarge:
|
||||
description: Payload exceeds configured limit (1 MiB).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
RateLimited:
|
||||
description: Request rejected by configured rate/volume limits.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
ValidationError:
|
||||
description: Request body or parameters failed validation.
|
||||
content:
|
||||
@@ -338,6 +624,9 @@ components:
|
||||
type: string
|
||||
enum:
|
||||
- unauthorized
|
||||
- forbidden
|
||||
- conflict
|
||||
- agent_blocked
|
||||
- not_found
|
||||
- validation
|
||||
- payload_too_large
|
||||
@@ -355,6 +644,9 @@ components:
|
||||
next_cursor:
|
||||
type: [string, "null"]
|
||||
description: Pass back as `cursor` to fetch the next page. Null/absent if no more results.
|
||||
prev_cursor:
|
||||
type: [string, "null"]
|
||||
description: Pass back as `cursor` with `back=true` to fetch the previous page. Null on the first page.
|
||||
AcceptedResponse:
|
||||
type: object
|
||||
required: [accepted]
|
||||
@@ -438,7 +730,7 @@ components:
|
||||
description: |
|
||||
Event body used inside `EventBatchRequest.events`. The owning `agent_id`
|
||||
is supplied once at the batch envelope; per-event `agent_id` is intentionally
|
||||
absent so requests cannot mix or spoof other agents under a shared token.
|
||||
absent so one HTTP request cannot mix multiple agents.
|
||||
Server response endpoints (`GET /events/query`) include `agent_id` separately
|
||||
in their item schema.
|
||||
required:
|
||||
@@ -510,7 +802,7 @@ components:
|
||||
format: date-time
|
||||
Agent:
|
||||
type: object
|
||||
required: [agent_id, hostname, status, last_seen_at, features]
|
||||
required: [agent_id, hostname, status, admission_state, last_seen_at, features]
|
||||
properties:
|
||||
agent_id:
|
||||
type: string
|
||||
@@ -519,6 +811,17 @@ components:
|
||||
status:
|
||||
type: string
|
||||
enum: [alive, stale, dead]
|
||||
admission_state:
|
||||
type: string
|
||||
enum: [pending, accepted]
|
||||
rotation_pending:
|
||||
type: boolean
|
||||
default: false
|
||||
description: True when a pending key would replace an already accepted key for this agent.
|
||||
key_fingerprint:
|
||||
type: [string, "null"]
|
||||
minLength: 16
|
||||
maxLength: 16
|
||||
last_seen_at:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -528,6 +831,36 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
AgentBlacklistItem:
|
||||
type: object
|
||||
required: [id, key_fingerprint, agent_id, hostname, created_at, last_seen_at]
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
minLength: 64
|
||||
maxLength: 64
|
||||
description: Opaque blacklist row id. Use for delete operations; display `key_fingerprint` to humans.
|
||||
key_fingerprint:
|
||||
type: string
|
||||
minLength: 16
|
||||
maxLength: 16
|
||||
agent_id:
|
||||
type: string
|
||||
hostname:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
last_seen_at:
|
||||
type: string
|
||||
format: date-time
|
||||
ActionCountResponse:
|
||||
type: object
|
||||
required: [count]
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
CheckState:
|
||||
type: object
|
||||
required: [agent_id, check_id, status, last_observed_at]
|
||||
@@ -602,3 +935,40 @@ components:
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
SystemSummaryResponse:
|
||||
type: object
|
||||
description: Bounded aggregate counts for the web navigation.
|
||||
required:
|
||||
- agents_online
|
||||
- agents_offline
|
||||
- checks_ok
|
||||
- checks_warning
|
||||
- checks_critical
|
||||
- checks_unknown
|
||||
- open_incidents
|
||||
- generated_at
|
||||
properties:
|
||||
agents_online:
|
||||
type: integer
|
||||
minimum: 0
|
||||
agents_offline:
|
||||
type: integer
|
||||
minimum: 0
|
||||
checks_ok:
|
||||
type: integer
|
||||
minimum: 0
|
||||
checks_warning:
|
||||
type: integer
|
||||
minimum: 0
|
||||
checks_critical:
|
||||
type: integer
|
||||
minimum: 0
|
||||
checks_unknown:
|
||||
type: integer
|
||||
minimum: 0
|
||||
open_incidents:
|
||||
type: integer
|
||||
minimum: 0
|
||||
generated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Reference in New Issue
Block a user