Files
certctl/api/openapi.yaml
T
shankar0123 0725713e19 Close I-004 (agent hard-delete cascades targets) coverage-gap finding
Operator decision answered as full soft-delete with optional forced
cascade — hard-delete is not reachable from any public surface. Prior
to this commit, DELETE /agents/{id} ran a plain `DELETE FROM agents`
whose schema-level `ON DELETE CASCADE` on deployment_targets.agent_id
silently wiped every target, orphaning certs and aborting in-flight
jobs. The finding closure reshapes the agent-removal contract around
soft retirement with explicit preflight counts, an opt-in cascade
gated by a mandatory reason, and unconditional protection for the
four reserved sentinel agents used by discovery sources.

Schema — migration 000015:
  migrations/000015_agent_retire.up.sql flips
  deployment_targets_agent_id_fkey from ON DELETE CASCADE to ON DELETE
  RESTRICT, so a stray `DELETE FROM agents` now errors at the DB
  boundary instead of quietly destroying targets. Both `agents` and
  `deployment_targets` grow a retired_at TIMESTAMPTZ + retired_reason
  TEXT pair (TEXT not VARCHAR so operator comments are never
  truncated), indexed via partial indexes WHERE retired_at IS NOT
  NULL. The migration is self-healing (ADD COLUMN IF NOT EXISTS, DROP
  CONSTRAINT IF EXISTS then ADD CONSTRAINT, CREATE INDEX IF NOT
  EXISTS) so repeated runs against partially-migrated databases
  converge. migrations/000015_agent_retire.down.sql restores CASCADE
  and drops the new columns for clean rollback. A dedicated
  repository-layer testcontainers test
  (internal/repository/postgres/migration_000015_test.go) asserts the
  before/after FK action, column presence, index presence, and
  round-trip idempotency under up→down→up.

Domain — sentinel guard + dependency counts:
  internal/domain/connector.go gains IsRetired() on Agent, the
  exported SentinelAgentIDs slice listing server-scanner,
  cloud-aws-sm, cloud-azure-kv, cloud-gcp-sm verbatim (matching the
  four reserved IDs documented in CLAUDE.md and created at startup in
  cmd/server/main.go), IsSentinelAgent(id string) predicate,
  AgentDependencyCounts{ActiveTargets, ActiveCertificates,
  PendingJobs} with a HasDependencies() method, and ActorTypeAgent /
  ActorTypeSystem enum values used by audit emission downstream.
  Coverage locked down by internal/domain/connector_test.go.

Service — 8-step ordered contract:
  internal/service/agent_retire.go:RetireAgent(ctx, id, actor,
  opts{Force, Reason}) enforces a fixed execution order:
  (1) sentinel guard — IsSentinelAgent(id) returns ErrAgentIsSentinel
      unconditionally; force=true does NOT bypass it.
  (2) fetch — ErrAgentNotFound on miss.
  (3) idempotency — if IsRetired() already, return
      AgentRetirementResult{AlreadyRetired: true} with no new audit
      event and no state change (safe to replay from flaky clients).
  (4) preflight counts — collectAgentDependencyCounts runs
      ActiveTargets, ActiveCertificates, PendingJobs sequentially
      (not in parallel; keeps the per-query timeout predictable and
      matches the repo's existing call-chain shape).
  (5) force-reason guard — opts.Force=true with empty Reason returns
      ErrForceReasonRequired (wired into the 400 status surface).
  (6) dependency guard — HasDependencies() with opts.Force=false
      returns BlockedByDependenciesError{Counts} (wired into the 409
      body with per-bucket counts).
  (7) mutation — single pinned retiredAt := time.Now(); agent
      retirement first, then cascade target retirement if opts.Force,
      all under the repo's single transaction so the two retired_at
      stamps match to the second.
  (8) best-effort audit — agent_retired always; agent_retirement_
      cascaded additionally on the force path. Actor is whatever the
      handler resolves from the request; actor type is mapped by
      resolveActorType (system/agent-prefix→Agent/else→User). Audit
      emission failures are logged via slog.Error but do not abort
      the retirement (matches the house convention used by every
      other scheduler-emitted event).

  BlockedByDependenciesError implements Error() as
  "active_targets=%d, active_certificates=%d, pending_jobs=%d" and
  Unwrap() → ErrBlockedByDependencies. The single struct satisfies
  errors.Is via Unwrap (used by scheduler-level tests) and errors.As
  via the concrete type (used by the handler to fish out Counts for
  the 409 body). ListRetiredAgents(page, perPage) adds a separate
  paginated accessor with page<1→1 and perPage<1→50 normalization so
  retired rows are queryable without polluting the default agent
  listing.

  Sentinel guard coverage is asymmetric by design: all four reserved
  IDs are protected, and force=true cannot override. Regression tests
  in internal/service/agent_retire_test.go assert each of the eight
  steps in order, plus sentinel bypass attempts and idempotency
  replay.

Handler + router — status-code surface:
  internal/api/handler/agents.go:RetireAgent exposes seven status
  codes on DELETE /agents/{id}:
    200 on a fresh retirement (body echoes AgentRetirementResult).
    204 on idempotent replay (AlreadyRetired=true; no new audit).
    400 on ErrForceReasonRequired.
    403 on ErrAgentIsSentinel.
    404 on ErrAgentNotFound.
    409 on BlockedByDependenciesError, with a custom body shape
        {error, counts{active_targets, active_certificates,
        pending_jobs}} that bypasses the default ErrorWithRequestID
        envelope so callers get the per-bucket numbers directly.
    500 on any other error.
  Heartbeat HandleHeartbeat returns 410 Gone when the agent is
  retired (ErrAgentRetired), signalling the agent to shut down.
  Query params `force=true` and `reason=<text>` drive the cascade
  path; both are forwarded as url.Values through the new MCP
  transport.

  internal/api/router/router.go registers GET /api/v1/agents/retired
  literal-path BEFORE /api/v1/agents/{id} — Go 1.22 ServeMux's
  literal-beats-pattern-var precedence routes "retired" to the
  paginated retired-agents listing instead of fetching a hypothetical
  agent named "retired".

Agent binary — clean shutdown on 410:
  cmd/agent/main.go gains the ErrAgentRetired sentinel, a
  retiredOnce sync.Once, and a retiredSignal chan struct{}. A
  markRetired(source, statusCode, body) helper closes the channel
  exactly once; the Run() select loop observes the close and returns
  ErrAgentRetired; main() matches via errors.Is(err, ErrAgentRetired)
  and exits cleanly instead of spinning in the heartbeat retry loop.
  The 410 Gone surface is therefore terminal for the agent process.

MCP transport:
  internal/mcp/client.go adds Client.DeleteWithQuery(path, query),
  a new additive transport method. Client.Delete is path-only; without
  this method the retire tool would silently drop `force` and `reason`,
  turning every cascade retire into a default soft-retire. The new
  method shares do()'s 204 normalization and 4xx/5xx error
  propagation so tool authors get one contract.
  internal/mcp/tools.go + internal/mcp/types.go expose the
  retire_agent tool with Force+Reason inputs wired through
  DeleteWithQuery.

CLI:
  cmd/cli/main.go + internal/cli/client.go add two CLI surfaces:
  `agents list --retired` (client-side strip of --retired then
  delegation to ListRetiredAgents, sharing --page/--per-page parsing
  with the default listing) and `agents retire <id> [--force --reason
  "…"]` (mirrors ErrForceReasonRequired — force without reason is
  rejected client-side before the request is sent). JSON + table
  output modes both honor the new columns.

Frontend:
  web/src/pages/AgentsPage.tsx surfaces retired/retire affordances.
  web/src/api/client.ts + web/src/api/types.ts expose the retire
  endpoint and the retired-listing. 4 new Vitest regression cases.

OpenAPI:
  api/openapi.yaml documents DELETE /agents/{id} with all seven
  status codes, 410 on heartbeat, and the 409 per-bucket body shape.

Regression coverage (six new test files, all green):
  internal/service/agent_retire_test.go           — 8-step contract + sentinel guards
  internal/api/handler/agent_retire_handler_test.go — 7-status-code surface + 410 heartbeat
  internal/mcp/retire_agent_test.go               — DeleteWithQuery wire-through
  internal/cli/agent_retire_test.go               — --retired listing + --force/--reason pairing
  internal/repository/postgres/migration_000015_test.go — FK flip + columns + indexes + up↔down
  internal/domain/connector_test.go               — IsRetired, IsSentinelAgent, SentinelAgentIDs, HasDependencies

Files:
  api/openapi.yaml                                — DELETE + 410 + 409 body shape
  cmd/agent/main.go                               — ErrAgentRetired, markRetired, retiredSignal
  cmd/cli/main.go                                 — handleAgents list/get/retire dispatch
  docs/architecture.md, docs/concepts.md,
    docs/testing-guide.md                         — retirement contract narrative
  internal/api/handler/agents.go                  — RetireAgent, status surface, 410 on heartbeat
  internal/api/handler/agent_handler_test.go      — extended coverage
  internal/api/handler/agent_retire_handler_test.go — new
  internal/api/router/router.go                   — /agents/retired before /agents/{id}
  internal/cli/agent_retire_test.go               — new
  internal/cli/client.go                          — ListRetiredAgents + RetireAgent
  internal/domain/connector.go                    — IsRetired, SentinelAgentIDs,
                                                    IsSentinelAgent, AgentDependencyCounts,
                                                    ActorTypeAgent/System
  internal/domain/connector_test.go               — new
  internal/integration/lifecycle_test.go          — retirement fixture
  internal/mcp/client.go                          — DeleteWithQuery additive transport
  internal/mcp/retire_agent_test.go               — new
  internal/mcp/tools.go, internal/mcp/types.go    — retire_agent tool + Force/Reason inputs
  internal/repository/interfaces.go               — AgentRepository retirement methods
  internal/repository/postgres/agent.go           — retire + cascade target retire + counts
  internal/repository/postgres/migration_000015_test.go — new
  internal/service/agent.go                       — wire into AgentService surface
  internal/service/agent_retire.go                — new 8-step contract
  internal/service/agent_retire_test.go           — new
  internal/service/deployment.go                  — skip retired agents
  internal/service/target.go                      — skip retired agents
  internal/service/testutil_test.go               — shared mocks extended
  migrations/000015_agent_retire.up.sql           — new
  migrations/000015_agent_retire.down.sql         — new
  web/src/api/client.ts, types.ts + tests         — retire endpoint wiring
  web/src/pages/AgentsPage.tsx                    — retire UI
2026-04-19 05:24:00 +00:00

4395 lines
136 KiB
YAML

openapi: 3.1.0
info:
title: certctl API
description: |
Certificate lifecycle management platform API. Manages certificates, issuers,
deployment targets, agents, jobs, policies, profiles, teams, owners, agent groups,
audit events, notifications, and observability metrics.
All endpoints under `/api/v1/` require authentication by default (configurable via
`CERTCTL_AUTH_TYPE`). Use `Bearer {api_key}` in the Authorization header.
Paginated list endpoints accept `page` (default 1) and `per_page` (default 50, max 500)
query parameters and return a standard envelope with `data`, `total`, `page`, and `per_page`.
version: 2.0.0
license:
name: BSL 1.1
url: https://github.com/shankar0123/certctl/blob/master/LICENSE
servers:
- url: http://localhost:8080
description: Local development
- url: http://localhost:8443
description: Docker Compose demo
security:
- bearerAuth: []
tags:
- name: Certificates
description: Certificate lifecycle — CRUD, versions, renewal, deployment, revocation
- name: CRL & OCSP
description: |
Certificate revocation list (RFC 5280) and OCSP responder (RFC 6960).
Served unauthenticated under `/.well-known/pki/*` (RFC 8615) so
relying parties can retrieve revocation status without a certctl
API key.
- name: Issuers
description: CA issuer connector management (Local CA, ACME, step-ca)
- name: Targets
description: Deployment target management (NGINX, Apache, HAProxy, F5, IIS)
- name: Agents
description: Agent registration, heartbeat, CSR submission, work polling
- name: Jobs
description: Job queue — issuance, renewal, deployment, validation
- name: Policies
description: Policy rules and violation tracking
- name: Profiles
description: Certificate enrollment profiles with crypto constraints
- name: Teams
description: Team management for ownership grouping
- name: Owners
description: Certificate owner management with email routing
- name: Agent Groups
description: Dynamic agent grouping by OS, architecture, IP CIDR, version
- name: Audit
description: Immutable audit trail
- name: Notifications
description: Notification events (expiration, renewal, deployment, revocation)
- name: Stats
description: Dashboard statistics and aggregations
- name: Metrics
description: System metrics (gauges, counters, uptime)
- name: Health
description: Health and readiness probes, auth info
- name: Discovery
description: Certificate discovery — filesystem scanning by agents and network TLS probing
- name: Network Scan
description: Network scan target management for active TLS certificate discovery
- name: Health Monitoring
description: Continuous TLS endpoint health checks with status tracking and probe history
- name: Digest
description: Scheduled certificate digest email notifications
- name: Verification
description: Post-deployment TLS endpoint fingerprint verification
- name: EST
description: Enrollment over Secure Transport (RFC 7030)
- name: SCEP
description: Simple Certificate Enrollment Protocol (RFC 8894)
paths:
# ─── Health & Auth ───────────────────────────────────────────────────
/health:
get:
tags: [Health]
summary: Health check
security: []
operationId: getHealth
responses:
"200":
description: Server is healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: healthy
/ready:
get:
tags: [Health]
summary: Readiness check
security: []
operationId: getReady
responses:
"200":
description: Server is ready
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ready
/api/v1/auth/info:
get:
tags: [Health]
summary: Auth configuration info
description: Returns auth mode. Served without auth so GUI can detect auth requirements before login.
security: []
operationId: getAuthInfo
responses:
"200":
description: Auth configuration
content:
application/json:
schema:
type: object
properties:
auth_type:
type: string
enum: [api-key, jwt, none]
required:
type: boolean
/api/v1/auth/check:
get:
tags: [Health]
summary: Validate credentials
description: Returns 200 if auth credentials are valid, 401 otherwise.
operationId: checkAuth
responses:
"200":
description: Authenticated
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: authenticated
"401":
description: Unauthorized
# ─── Certificates ────────────────────────────────────────────────────
/api/v1/certificates:
get:
tags: [Certificates]
summary: List certificates
operationId: listCertificates
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
- name: status
in: query
schema:
$ref: "#/components/schemas/CertificateStatus"
- name: environment
in: query
schema:
type: string
- name: owner_id
in: query
schema:
type: string
- name: team_id
in: query
schema:
type: string
- name: issuer_id
in: query
schema:
type: string
responses:
"200":
description: Paginated list of certificates
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/ManagedCertificate"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Certificates]
summary: Create certificate
operationId: createCertificate
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedCertificate"
responses:
"201":
description: Certificate created
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedCertificate"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}:
get:
tags: [Certificates]
summary: Get certificate
operationId: getCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Certificate details
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedCertificate"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Certificates]
summary: Update certificate
operationId: updateCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedCertificate"
responses:
"200":
description: Certificate updated
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedCertificate"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Certificates]
summary: Archive certificate
operationId: archiveCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Certificate archived
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}/versions:
get:
tags: [Certificates]
summary: List certificate versions
operationId: listCertificateVersions
parameters:
- $ref: "#/components/parameters/resourceId"
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of certificate versions
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/CertificateVersion"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}/renew:
post:
tags: [Certificates]
summary: Trigger certificate renewal
operationId: triggerRenewal
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"202":
description: Renewal triggered
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"409":
$ref: "#/components/responses/Conflict"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}/deploy:
post:
tags: [Certificates]
summary: Trigger certificate deployment
operationId: triggerDeployment
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
target_id:
type: string
description: Optional specific target ID
responses:
"202":
description: Deployment triggered
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}/revoke:
post:
tags: [Certificates]
summary: Revoke certificate
description: |
Revokes a certificate with an optional RFC 5280 reason code. Records revocation in
cert inventory, audit log, and certificate_revocations table. Best-effort issuer notification.
operationId: revokeCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
reason:
$ref: "#/components/schemas/RevocationReason"
responses:
"200":
description: Certificate revoked
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Bulk Revocation ─────────────────────────────────────────────────
/api/v1/certificates/bulk-revoke:
post:
tags: [Certificates]
summary: Bulk revoke certificates
description: |
Revokes all certificates matching the given filter criteria. At least one criterion
is required (safety guard against accidental mass revocation). Reuses the single-cert
revocation flow per certificate with partial-failure tolerance.
operationId: bulkRevokeCertificates
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BulkRevokeRequest"
responses:
"200":
description: Bulk revocation result
content:
application/json:
schema:
$ref: "#/components/schemas/BulkRevokeResult"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Certificate Export ──────────────────────────────────────────────
/api/v1/certificates/{id}/export/pem:
get:
tags: [Certificates]
summary: Export certificate as PEM
description: |
Returns the certificate and its chain in PEM format. By default returns JSON
with cert_pem, chain_pem, and full_pem fields. Add ?download=true to get the
full PEM chain as a file download with Content-Disposition headers.
operationId: exportCertificatePEM
parameters:
- $ref: "#/components/parameters/resourceId"
- name: download
in: query
schema:
type: string
enum: ["true"]
description: Set to "true" to get a file download instead of JSON.
responses:
"200":
description: PEM export
content:
application/json:
schema:
type: object
properties:
cert_pem:
type: string
description: Leaf certificate PEM
chain_pem:
type: string
description: Intermediate/root chain PEM
full_pem:
type: string
description: Full PEM chain (cert + intermediates)
application/x-pem-file:
schema:
type: string
format: binary
description: Full PEM file (when download=true)
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/certificates/{id}/export/pkcs12:
post:
tags: [Certificates]
summary: Export certificate as PKCS#12
description: |
Returns a PKCS#12 (.p12) bundle containing the certificate and chain.
Private keys are NOT included — they live on agents and never touch the control plane.
The bundle is encrypted with the provided password (or empty password if omitted).
operationId: exportCertificatePKCS12
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
password:
type: string
description: Password to encrypt the PKCS#12 bundle (can be empty)
responses:
"200":
description: PKCS#12 binary
content:
application/x-pkcs12:
schema:
type: string
format: binary
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── PKI (CRL & OCSP, RFC 5280 / 6960 / 8615) ──────────────────────
#
# Relying parties (browsers, OpenSSL clients, OCSP stapling sidecars,
# mTLS clients) cannot present a certctl Bearer token, so these two
# endpoints are unauthenticated and live under the RFC 8615
# `.well-known` namespace. They were previously mounted at
# /api/v1/crl/{issuer_id} and /api/v1/ocsp/{issuer_id}/{serial}; those
# paths were removed in M-006.
#
# The non-standard JSON CRL endpoint (GET /api/v1/crl) was also
# removed — RFC 5280 defines only the DER wire format.
/.well-known/pki/crl/{issuer_id}:
get:
tags: [CRL & OCSP]
summary: Get DER-encoded X.509 CRL (RFC 5280)
description: |
Returns a DER-encoded CRL signed by the issuing CA (RFC 5280 §5),
served unauthenticated per RFC 8615 `.well-known` semantics so
relying parties can retrieve it without a certctl API key.
Validity is 24 hours.
operationId: getDERCRL
security: []
parameters:
- name: issuer_id
in: path
required: true
schema:
type: string
responses:
"200":
description: DER-encoded CRL
content:
application/pkix-crl:
schema:
type: string
format: binary
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
"501":
description: Issuer does not support CRL generation
/.well-known/pki/ocsp/{issuer_id}/{serial}:
get:
tags: [CRL & OCSP]
summary: OCSP responder (RFC 6960)
description: |
Returns a signed OCSP response (good/revoked/unknown) for the
given serial number per RFC 6960 §2.1, served unauthenticated
per RFC 8615 so relying parties and OCSP stapling sidecars can
query revocation status without a certctl API key.
operationId: handleOCSP
security: []
parameters:
- name: issuer_id
in: path
required: true
schema:
type: string
- name: serial
in: path
required: true
description: Hex-encoded certificate serial number
schema:
type: string
responses:
"200":
description: OCSP response
content:
application/ocsp-response:
schema:
type: string
format: binary
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
"501":
description: Issuer does not support OCSP
# ─── Issuers ─────────────────────────────────────────────────────────
/api/v1/issuers:
get:
tags: [Issuers]
summary: List issuers
operationId: listIssuers
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of issuers
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Issuer"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Issuers]
summary: Create issuer
operationId: createIssuer
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Issuer"
responses:
"201":
description: Issuer created
content:
application/json:
schema:
$ref: "#/components/schemas/Issuer"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/issuers/{id}:
get:
tags: [Issuers]
summary: Get issuer
operationId: getIssuer
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Issuer details
content:
application/json:
schema:
$ref: "#/components/schemas/Issuer"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Issuers]
summary: Update issuer
operationId: updateIssuer
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Issuer"
responses:
"200":
description: Issuer updated
content:
application/json:
schema:
$ref: "#/components/schemas/Issuer"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Issuers]
summary: Delete issuer
operationId: deleteIssuer
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Issuer deleted
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/issuers/{id}/test:
post:
tags: [Issuers]
summary: Test issuer connection
operationId: testIssuerConnection
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Connection successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Targets ─────────────────────────────────────────────────────────
/api/v1/targets:
get:
tags: [Targets]
summary: List targets
operationId: listTargets
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of targets
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/DeploymentTarget"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Targets]
summary: Create target
operationId: createTarget
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeploymentTarget"
responses:
"201":
description: Target created
content:
application/json:
schema:
$ref: "#/components/schemas/DeploymentTarget"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/targets/{id}:
get:
tags: [Targets]
summary: Get target
operationId: getTarget
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Target details
content:
application/json:
schema:
$ref: "#/components/schemas/DeploymentTarget"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Targets]
summary: Update target
operationId: updateTarget
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeploymentTarget"
responses:
"200":
description: Target updated
content:
application/json:
schema:
$ref: "#/components/schemas/DeploymentTarget"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Targets]
summary: Delete target
operationId: deleteTarget
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Target deleted
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/targets/{id}/test:
post:
tags: [Targets]
summary: Test target connection
description: |
Checks target connectivity by verifying the assigned agent's heartbeat status
(agent reported within the last 5 minutes). Always returns HTTP 200 — the
connectivity result is reflected in the response body's `status` field
(`success` when the agent is reachable, `failed` otherwise).
operationId: testTargetConnection
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Connection test result (success or failed in body)
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"400":
$ref: "#/components/responses/BadRequest"
# ─── Agents ──────────────────────────────────────────────────────────
/api/v1/agents:
get:
tags: [Agents]
summary: List agents
operationId: listAgents
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of agents
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Agent"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Agents]
summary: Register agent
operationId: registerAgent
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Agent"
responses:
"201":
description: Agent registered
content:
application/json:
schema:
$ref: "#/components/schemas/Agent"
"400":
$ref: "#/components/responses/BadRequest"
"409":
$ref: "#/components/responses/Conflict"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/retired:
get:
tags: [Agents]
summary: List retired agents
description: |
I-004: opt-in listing of soft-retired agents. The default
`GET /api/v1/agents` endpoint filters retired rows out; this is the
dedicated surface for reading them back (e.g., the operator UI's
"Retired" tab, audit and forensics workflows). Pagination defaults
match the default agent listing (page=1, per_page=50, max 500). Go
1.22's enhanced ServeMux routes `/agents/retired` to this handler
via the literal-beats-pattern-var precedence rule, so the sibling
`/agents/{id}` route does not shadow it.
operationId: listRetiredAgents
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of retired agents
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Agent"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}:
get:
tags: [Agents]
summary: Get agent
operationId: getAgent
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Agent details
content:
application/json:
schema:
$ref: "#/components/schemas/Agent"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Agents]
summary: Soft-retire agent
description: |
I-004: soft-retirement. The agent row is preserved (so its audit
trail and historical job links remain intact) and `retired_at` is
stamped. A retired agent receives `410 Gone` on subsequent
heartbeats so it can shut down cleanly.
Behavior matrix:
| Scenario | Query | Status | Body |
| --- | --- | --- | --- |
| Clean retire (no active dependencies) | none | `200` | `RetireAgentResponse` with `cascade=false`, zero counts |
| Blocked by active targets/certs/jobs | none | `409` | `BlockedByDependenciesResponse` with per-bucket counts |
| Force-cascade retire | `force=true&reason=...` | `200` | `RetireAgentResponse` with `cascade=true`, pre-cascade counts |
| Idempotent re-retire | either | `204` | (empty — downstream consumers break on stray bodies) |
| `force=true` without reason | `force=true` | `400` | ErrorResponse (ErrForceReasonRequired) |
| Reserved sentinel agent | any | `403` | ErrorResponse (ErrAgentIsSentinel) |
| Unknown agent id | any | `404` | ErrorResponse |
Sentinel agents are the four reserved identities backing non-agent
discovery subsystems (`server-scanner`, `cloud-aws-sm`,
`cloud-azure-kv`, `cloud-gcp-sm`). Retiring them would orphan the
scanner or a cloud secret-manager source, so the handler refuses
unconditionally — even with `force=true`.
operationId: retireAgent
parameters:
- $ref: "#/components/parameters/resourceId"
- name: force
in: query
required: false
schema:
type: boolean
default: false
description: |
Cascade-retire active downstream targets, certificates, and
jobs. When `true`, a non-empty `reason` is required. A
malformed value (anything strconv.ParseBool rejects) is
silently treated as `false` so a typoed query can never
accidentally enable the cascade.
- name: reason
in: query
required: false
schema:
type: string
description: |
Human-readable reason recorded on the retired row and in the
immutable audit trail. Required (non-empty after trimming)
when `force=true`.
responses:
"200":
description: |
Agent retired (clean retire or successful force-cascade). Body
is `RetireAgentResponse`.
content:
application/json:
schema:
$ref: "#/components/schemas/RetireAgentResponse"
"204":
description: |
Idempotent retire — the agent was already retired. Response
body is empty (the 200-path shape does not apply, and
downstream clients that tee responses into dashboards would
break on spurious bodies).
"400":
description: |
`force=true` was sent without a non-empty `reason`
(ErrForceReasonRequired).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"403":
description: |
Agent is a reserved sentinel and cannot be retired even with
`?force=true` (ErrAgentIsSentinel).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: |
Blocked by active downstream dependencies. Body carries
per-bucket counts so the operator UI can show the user which
dependency is holding up the retire. Re-run with
`?force=true&reason=...` to cascade.
content:
application/json:
schema:
$ref: "#/components/schemas/BlockedByDependenciesResponse"
"405":
description: Method not allowed (only DELETE, GET are routed to this path)
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}/heartbeat:
post:
tags: [Agents]
summary: Agent heartbeat
description: |
Reports agent liveness and metadata (OS, architecture, IP, version).
I-004: a retired agent still polling the heartbeat endpoint receives
`410 Gone` so `cmd/agent` detects the terminal signal and shuts down
cleanly instead of looping forever against a decommissioned identity.
The retired-agent check runs before any "not found" string match so
it can never be masked by a sibling error branch.
operationId: agentHeartbeat
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
version:
type: string
hostname:
type: string
os:
type: string
architecture:
type: string
ip_address:
type: string
responses:
"200":
description: Heartbeat recorded
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"410":
description: |
I-004: the agent has been soft-retired. The agent process should
treat this as a terminal signal and shut down cleanly.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}/csr:
post:
tags: [Agents]
summary: Submit CSR
description: Agent submits a PEM-encoded CSR for signing. Used in agent keygen mode.
operationId: agentSubmitCSR
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [csr_pem]
properties:
csr_pem:
type: string
description: PEM-encoded certificate signing request
certificate_id:
type: string
responses:
"202":
description: CSR accepted
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}/certificates/{cert_id}:
get:
tags: [Agents]
summary: Pick up signed certificate
description: Agent retrieves the signed certificate PEM after CSR signing completes.
operationId: agentPickupCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
- name: cert_id
in: path
required: true
schema:
type: string
responses:
"200":
description: Certificate PEM
content:
application/json:
schema:
type: object
properties:
certificate_pem:
type: string
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}/work:
get:
tags: [Agents]
summary: Get pending work
description: Returns pending deployment and AwaitingCSR jobs for the agent.
operationId: agentGetWork
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Work items
content:
application/json:
schema:
type: object
properties:
jobs:
type: array
items:
$ref: "#/components/schemas/WorkItem"
count:
type: integer
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents/{id}/jobs/{job_id}/status:
post:
tags: [Agents]
summary: Report job status
description: Agent reports completion or failure of an assigned job.
operationId: agentReportJobStatus
parameters:
- $ref: "#/components/parameters/resourceId"
- name: job_id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [status]
properties:
status:
type: string
error:
type: string
responses:
"200":
description: Status updated
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Jobs ────────────────────────────────────────────────────────────
/api/v1/jobs:
get:
tags: [Jobs]
summary: List jobs
operationId: listJobs
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
- name: status
in: query
schema:
$ref: "#/components/schemas/JobStatus"
- name: type
in: query
schema:
$ref: "#/components/schemas/JobType"
responses:
"200":
description: Paginated list of jobs
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Job"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}:
get:
tags: [Jobs]
summary: Get job
operationId: getJob
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Job details
content:
application/json:
schema:
$ref: "#/components/schemas/Job"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}/cancel:
post:
tags: [Jobs]
summary: Cancel job
operationId: cancelJob
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Job cancelled
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}/approve:
post:
tags: [Jobs]
summary: Approve job
description: Approves a job in AwaitingApproval state.
operationId: approveJob
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Job approved
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}/reject:
post:
tags: [Jobs]
summary: Reject job
description: Rejects a job in AwaitingApproval state with an optional reason.
operationId: rejectJob
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
reason:
type: string
responses:
"200":
description: Job rejected
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}/verify:
post:
tags: [Verification]
summary: Record post-deployment verification result
description: |
Agents submit the result of probing a deployed certificate's live TLS endpoint.
Compares the served certificate's SHA-256 fingerprint against the expected
fingerprint. Best-effort: failures are recorded on the job but do not roll
back the deployment.
operationId: verifyDeployment
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/VerifyDeploymentRequest"
responses:
"200":
description: Verification result recorded
content:
application/json:
schema:
type: object
properties:
job_id:
type: string
verified:
type: boolean
verified_at:
type: string
format: date-time
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/jobs/{id}/verification:
get:
tags: [Verification]
summary: Get post-deployment verification status
description: |
Returns the stored verification result for a deployment job — expected
and observed SHA-256 fingerprints, verified flag, and timestamp.
operationId: getJobVerification
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Verification result for the job
content:
application/json:
schema:
$ref: "#/components/schemas/VerificationResult"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Policies ────────────────────────────────────────────────────────
/api/v1/policies:
get:
tags: [Policies]
summary: List policies
operationId: listPolicies
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of policies
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/PolicyRule"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Policies]
summary: Create policy
operationId: createPolicy
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
responses:
"201":
description: Policy created
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/policies/{id}:
get:
tags: [Policies]
summary: Get policy
operationId: getPolicy
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Policy details
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Policies]
summary: Update policy
operationId: updatePolicy
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
responses:
"200":
description: Policy updated
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyRule"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Policies]
summary: Delete policy
operationId: deletePolicy
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Policy deleted
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/policies/{id}/violations:
get:
tags: [Policies]
summary: List policy violations
operationId: listPolicyViolations
parameters:
- $ref: "#/components/parameters/resourceId"
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of violations
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/PolicyViolation"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Profiles ────────────────────────────────────────────────────────
/api/v1/profiles:
get:
tags: [Profiles]
summary: List profiles
operationId: listProfiles
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of profiles
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/CertificateProfile"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Profiles]
summary: Create profile
operationId: createProfile
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CertificateProfile"
responses:
"201":
description: Profile created
content:
application/json:
schema:
$ref: "#/components/schemas/CertificateProfile"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/profiles/{id}:
get:
tags: [Profiles]
summary: Get profile
operationId: getProfile
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Profile details
content:
application/json:
schema:
$ref: "#/components/schemas/CertificateProfile"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Profiles]
summary: Update profile
operationId: updateProfile
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CertificateProfile"
responses:
"200":
description: Profile updated
content:
application/json:
schema:
$ref: "#/components/schemas/CertificateProfile"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Profiles]
summary: Delete profile
operationId: deleteProfile
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Profile deleted
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Teams ───────────────────────────────────────────────────────────
/api/v1/teams:
get:
tags: [Teams]
summary: List teams
operationId: listTeams
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of teams
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Team"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Teams]
summary: Create team
operationId: createTeam
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Team"
responses:
"201":
description: Team created
content:
application/json:
schema:
$ref: "#/components/schemas/Team"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/teams/{id}:
get:
tags: [Teams]
summary: Get team
operationId: getTeam
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Team details
content:
application/json:
schema:
$ref: "#/components/schemas/Team"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Teams]
summary: Update team
operationId: updateTeam
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Team"
responses:
"200":
description: Team updated
content:
application/json:
schema:
$ref: "#/components/schemas/Team"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Teams]
summary: Delete team
operationId: deleteTeam
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Team deleted
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Owners ──────────────────────────────────────────────────────────
/api/v1/owners:
get:
tags: [Owners]
summary: List owners
operationId: listOwners
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of owners
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Owner"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Owners]
summary: Create owner
operationId: createOwner
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Owner"
responses:
"201":
description: Owner created
content:
application/json:
schema:
$ref: "#/components/schemas/Owner"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/owners/{id}:
get:
tags: [Owners]
summary: Get owner
operationId: getOwner
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Owner details
content:
application/json:
schema:
$ref: "#/components/schemas/Owner"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Owners]
summary: Update owner
operationId: updateOwner
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Owner"
responses:
"200":
description: Owner updated
content:
application/json:
schema:
$ref: "#/components/schemas/Owner"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Owners]
summary: Delete owner
operationId: deleteOwner
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Owner deleted
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Agent Groups ───────────────────────────────────────────────────
/api/v1/agent-groups:
get:
tags: [Agent Groups]
summary: List agent groups
operationId: listAgentGroups
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of agent groups
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/AgentGroup"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Agent Groups]
summary: Create agent group
operationId: createAgentGroup
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AgentGroup"
responses:
"201":
description: Agent group created
content:
application/json:
schema:
$ref: "#/components/schemas/AgentGroup"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agent-groups/{id}:
get:
tags: [Agent Groups]
summary: Get agent group
operationId: getAgentGroup
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Agent group details
content:
application/json:
schema:
$ref: "#/components/schemas/AgentGroup"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Agent Groups]
summary: Update agent group
operationId: updateAgentGroup
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AgentGroup"
responses:
"200":
description: Agent group updated
content:
application/json:
schema:
$ref: "#/components/schemas/AgentGroup"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Agent Groups]
summary: Delete agent group
operationId: deleteAgentGroup
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Agent group deleted
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agent-groups/{id}/members:
get:
tags: [Agent Groups]
summary: List agent group members
description: Returns agents matching the group's dynamic criteria plus manually included members.
operationId: listAgentGroupMembers
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: List of member agents
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Agent"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Audit ───────────────────────────────────────────────────────────
/api/v1/audit:
get:
tags: [Audit]
summary: List audit events
operationId: listAuditEvents
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of audit events
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/AuditEvent"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/audit/{id}:
get:
tags: [Audit]
summary: Get audit event
operationId: getAuditEvent
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Audit event details
content:
application/json:
schema:
$ref: "#/components/schemas/AuditEvent"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Notifications ──────────────────────────────────────────────────
/api/v1/notifications:
get:
tags: [Notifications]
summary: List notifications
operationId: listNotifications
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: Paginated list of notifications
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/NotificationEvent"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/notifications/{id}:
get:
tags: [Notifications]
summary: Get notification
operationId: getNotification
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Notification details
content:
application/json:
schema:
$ref: "#/components/schemas/NotificationEvent"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/notifications/{id}/read:
post:
tags: [Notifications]
summary: Mark notification as read
operationId: markNotificationAsRead
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Marked as read
content:
application/json:
schema:
$ref: "#/components/schemas/StatusResponse"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ─── Stats ───────────────────────────────────────────────────────────
/api/v1/stats/summary:
get:
tags: [Stats]
summary: Dashboard summary
operationId: getDashboardSummary
responses:
"200":
description: High-level system metrics
content:
application/json:
schema:
$ref: "#/components/schemas/DashboardSummary"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/stats/certificates-by-status:
get:
tags: [Stats]
summary: Certificate status breakdown
operationId: getCertificatesByStatus
responses:
"200":
description: Certificate counts by status
content:
application/json:
schema:
type: object
properties:
status_counts:
type: array
items:
type: object
properties:
status:
type: string
count:
type: integer
format: int64
"500":
$ref: "#/components/responses/InternalError"
/api/v1/stats/expiration-timeline:
get:
tags: [Stats]
summary: Expiration timeline
operationId: getExpirationTimeline
parameters:
- name: days
in: query
schema:
type: integer
default: 30
minimum: 1
maximum: 365
responses:
"200":
description: Certificates expiring per day
content:
application/json:
schema:
type: object
properties:
buckets:
type: array
items:
type: object
properties:
date:
type: string
format: date
count:
type: integer
format: int64
"500":
$ref: "#/components/responses/InternalError"
/api/v1/stats/job-trends:
get:
tags: [Stats]
summary: Job success/failure trends
operationId: getJobTrends
parameters:
- name: days
in: query
schema:
type: integer
default: 30
minimum: 1
maximum: 365
responses:
"200":
description: Job trends per day
content:
application/json:
schema:
type: object
properties:
trends:
type: array
items:
type: object
properties:
date:
type: string
format: date
completed:
type: integer
format: int64
failed:
type: integer
format: int64
"500":
$ref: "#/components/responses/InternalError"
/api/v1/stats/issuance-rate:
get:
tags: [Stats]
summary: Certificate issuance rate
operationId: getIssuanceRate
parameters:
- name: days
in: query
schema:
type: integer
default: 30
minimum: 1
maximum: 365
responses:
"200":
description: Issuance count per day
content:
application/json:
schema:
type: object
properties:
rate:
type: array
items:
type: object
properties:
date:
type: string
format: date
count:
type: integer
format: int64
"500":
$ref: "#/components/responses/InternalError"
# ─── Metrics ─────────────────────────────────────────────────────────
/api/v1/metrics:
get:
tags: [Metrics]
summary: System metrics
description: JSON metrics snapshot with gauges, counters, and uptime. See also /api/v1/metrics/prometheus for Prometheus exposition format.
operationId: getMetrics
responses:
"200":
description: Metrics snapshot
content:
application/json:
schema:
$ref: "#/components/schemas/MetricsResponse"
"500":
$ref: "#/components/responses/InternalError"
# ─── Prometheus Metrics (M22) ──────────────────────────────────────
/api/v1/metrics/prometheus:
get:
tags: [Metrics]
summary: Prometheus metrics
description: |
Prometheus exposition format metrics. Compatible with Prometheus, Grafana Agent,
Datadog Agent, Victoria Metrics, and any OpenMetrics scraper.
Returns 11 metrics with certctl_ prefix (8 gauges, 2 counters, 1 info).
operationId: getPrometheusMetrics
responses:
"200":
description: Prometheus text format
content:
text/plain:
schema:
type: string
description: "Prometheus exposition format (text/plain; version=0.0.4)"
"500":
$ref: "#/components/responses/InternalError"
# ─── Certificate Deployments (M20) ─────────────────────────────────
/api/v1/certificates/{id}/deployments:
get:
tags: [Certificates]
summary: List certificate deployments
description: Returns deployment targets associated with this certificate.
operationId: getCertificateDeployments
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Deployment targets for this certificate
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/DeploymentTarget"
total:
type: integer
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Discovery (M18b) ─────────────────────────────────────────────
/api/v1/agents/{id}/discoveries:
post:
tags: [Discovery]
summary: Submit discovery report
description: |
Agent submits a batch of discovered certificates from filesystem scanning.
Server deduplicates by (fingerprint, agent_id, source_path) and records scan metadata.
operationId: submitDiscoveryReport
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DiscoveryReport"
responses:
"202":
description: Report accepted and processed
content:
application/json:
schema:
$ref: "#/components/schemas/DiscoveryScan"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovered-certificates:
get:
tags: [Discovery]
summary: List discovered certificates
description: Returns discovered certificates with optional filters by agent and triage status.
operationId: listDiscoveredCertificates
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
- name: agent_id
in: query
schema:
type: string
description: Filter by discovering agent
- name: status
in: query
schema:
type: string
enum: [Unmanaged, Managed, Dismissed]
description: Filter by triage status
responses:
"200":
description: Paginated list of discovered certificates
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/DiscoveredCertificate"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovered-certificates/{id}:
get:
tags: [Discovery]
summary: Get discovered certificate
description: Returns a single discovered certificate by ID.
operationId: getDiscoveredCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Discovered certificate details
content:
application/json:
schema:
$ref: "#/components/schemas/DiscoveredCertificate"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovered-certificates/{id}/claim:
post:
tags: [Discovery]
summary: Claim discovered certificate
description: Links a discovered certificate to an existing managed certificate. Changes status to Managed.
operationId: claimDiscoveredCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [managed_certificate_id]
properties:
managed_certificate_id:
type: string
description: ID of the managed certificate to link to
responses:
"200":
description: Certificate claimed
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovered-certificates/{id}/dismiss:
post:
tags: [Discovery]
summary: Dismiss discovered certificate
description: Marks a discovered certificate as dismissed (excluded from triage queue).
operationId: dismissDiscoveredCertificate
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Certificate dismissed
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovery-scans:
get:
tags: [Discovery]
summary: List discovery scans
description: Returns history of discovery scan executions with optional agent filter.
operationId: listDiscoveryScans
parameters:
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
- name: agent_id
in: query
schema:
type: string
description: Filter by agent ID
responses:
"200":
description: Paginated list of discovery scans
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/DiscoveryScan"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/discovery-summary:
get:
tags: [Discovery]
summary: Discovery status summary
description: Returns aggregate counts of discovered certificates by triage status.
operationId: getDiscoverySummary
responses:
"200":
description: Status counts
content:
application/json:
schema:
type: object
properties:
Unmanaged:
type: integer
Managed:
type: integer
Dismissed:
type: integer
"500":
$ref: "#/components/responses/InternalError"
# ─── Network Scan Targets (M21) ───────────────────────────────────
/api/v1/network-scan-targets:
get:
tags: [Network Scan]
summary: List network scan targets
description: Returns all configured network scan targets with CIDR ranges and ports.
operationId: listNetworkScanTargets
responses:
"200":
description: List of network scan targets
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationEnvelope"
- type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/NetworkScanTarget"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Network Scan]
summary: Create network scan target
description: |
Creates a new network scan target. CIDR ranges are validated and capped at /20
(4096 IPs max per CIDR) to prevent accidental huge scans.
operationId: createNetworkScanTarget
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NetworkScanTargetCreate"
responses:
"201":
description: Target created
content:
application/json:
schema:
$ref: "#/components/schemas/NetworkScanTarget"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/network-scan-targets/{id}:
get:
tags: [Network Scan]
summary: Get network scan target
description: Returns a single network scan target by ID.
operationId: getNetworkScanTarget
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Network scan target details
content:
application/json:
schema:
$ref: "#/components/schemas/NetworkScanTarget"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Network Scan]
summary: Update network scan target
description: Updates an existing network scan target.
operationId: updateNetworkScanTarget
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NetworkScanTargetCreate"
responses:
"200":
description: Target updated
content:
application/json:
schema:
$ref: "#/components/schemas/NetworkScanTarget"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Network Scan]
summary: Delete network scan target
description: Deletes a network scan target.
operationId: deleteNetworkScanTarget
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Target deleted
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/network-scan-targets/{id}/scan:
post:
tags: [Network Scan]
summary: Trigger network scan
description: |
Triggers an immediate scan of the specified target. Scans all configured CIDRs and ports
concurrently (50 goroutines). Results feed into the discovery pipeline for deduplication.
operationId: triggerNetworkScan
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"202":
description: Scan completed with certificates found
content:
application/json:
schema:
$ref: "#/components/schemas/DiscoveryScan"
"200":
description: Scan completed, no certificates found
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Health Monitoring ─────────────────────────────────────────────
/api/v1/health-checks:
get:
tags: [Health Monitoring]
summary: List endpoint health checks
description: |
Lists all TLS endpoint health checks with optional filtering by status, certificate, or network scan target.
Includes current status, last probe results, and probe history summary.
operationId: listHealthChecks
parameters:
- name: status
in: query
schema:
type: string
enum: [Healthy, Degraded, Down, CertMismatch]
description: Filter by health status
- name: certificate_id
in: query
schema:
type: string
description: Filter by certificate ID
- name: network_scan_target_id
in: query
schema:
type: string
description: Filter by network scan target ID
- name: enabled
in: query
schema:
type: boolean
description: Filter by enabled/disabled state
- $ref: "#/components/parameters/page"
- $ref: "#/components/parameters/per_page"
responses:
"200":
description: List of health checks
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/EndpointHealthCheck"
total:
type: integer
page:
type: integer
per_page:
type: integer
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [Health Monitoring]
summary: Create health check
description: Creates a new manual health check for an endpoint.
operationId: createHealthCheck
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [endpoint, check_interval_seconds]
properties:
endpoint:
type: string
description: "host:port to monitor"
example: "api.example.com:443"
expected_fingerprint:
type: string
description: Expected certificate SHA-256 fingerprint (optional)
check_interval_seconds:
type: integer
minimum: 30
description: Probe frequency in seconds (default 300)
timeout_ms:
type: integer
description: TLS connection timeout in milliseconds
responses:
"201":
description: Health check created
content:
application/json:
schema:
$ref: "#/components/schemas/EndpointHealthCheck"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/health-checks/summary:
get:
tags: [Health Monitoring]
summary: Health check summary
description: Returns aggregate status counts for all health checks.
operationId: getHealthCheckSummary
responses:
"200":
description: Health check summary
content:
application/json:
schema:
type: object
properties:
healthy:
type: integer
degraded:
type: integer
down:
type: integer
cert_mismatch:
type: integer
"500":
$ref: "#/components/responses/InternalError"
/api/v1/health-checks/{id}:
get:
tags: [Health Monitoring]
summary: Get health check
operationId: getHealthCheck
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"200":
description: Health check detail
content:
application/json:
schema:
$ref: "#/components/schemas/EndpointHealthCheck"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
put:
tags: [Health Monitoring]
summary: Update health check
description: Update thresholds, interval, or expected fingerprint.
operationId: updateHealthCheck
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
expected_fingerprint:
type: string
check_interval_seconds:
type: integer
timeout_ms:
type: integer
enabled:
type: boolean
responses:
"200":
description: Health check updated
content:
application/json:
schema:
$ref: "#/components/schemas/EndpointHealthCheck"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
delete:
tags: [Health Monitoring]
summary: Delete health check
operationId: deleteHealthCheck
parameters:
- $ref: "#/components/parameters/resourceId"
responses:
"204":
description: Health check deleted
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/health-checks/{id}/history:
get:
tags: [Health Monitoring]
summary: Get probe history
description: Returns historical probe records with status, response times, and errors.
operationId: getHealthCheckHistory
parameters:
- $ref: "#/components/parameters/resourceId"
- name: limit
in: query
schema:
type: integer
default: 100
minimum: 1
maximum: 1000
description: Max number of records to return
responses:
"200":
description: Probe history
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/HealthHistoryEntry"
total:
type: integer
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/health-checks/{id}/acknowledge:
post:
tags: [Health Monitoring]
summary: Acknowledge incident
description: Mark a health check incident as acknowledged by the operator.
operationId: acknowledgeHealthCheckIncident
parameters:
- $ref: "#/components/parameters/resourceId"
requestBody:
content:
application/json:
schema:
type: object
properties:
acknowledged_by:
type: string
description: Operator name or ID
responses:
"200":
description: Incident acknowledged
content:
application/json:
schema:
$ref: "#/components/schemas/EndpointHealthCheck"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/InternalError"
# ─── Digest ────────────────────────────────────────────────────────
/api/v1/digest/preview:
get:
tags: [Digest]
summary: Preview digest email
description: |
Returns an HTML preview of the scheduled certificate digest email.
This includes a summary of certificate status, pending jobs, and expiring certificates.
operationId: previewDigest
responses:
"200":
description: HTML digest email preview
content:
text/html:
schema:
type: string
example: "<html>...</html>"
"503":
description: Digest service not configured
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/digest/send:
post:
tags: [Digest]
summary: Send digest email
description: |
Triggers immediate sending of the certificate digest email to configured recipients.
If no explicit recipients are configured, sends to certificate owners.
operationId: sendDigest
responses:
"200":
description: Digest sent successfully
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"503":
description: Digest service not configured
content:
application/json:
schema:
$ref: "#/components/schemas/StatusMessageResponse"
"500":
$ref: "#/components/responses/InternalError"
# ─── EST (RFC 7030) ────────────────────────────────────────────────
/.well-known/est/cacerts:
get:
tags: [EST]
summary: EST CA certificates distribution
description: |
Returns the CA certificate chain used to verify certctl-issued certificates.
Response is a base64-encoded degenerate PKCS#7 SignedData (certs-only) per
RFC 7030 §4.1.3.
operationId: estCACerts
security: []
responses:
"200":
description: Base64-encoded PKCS#7 certs-only structure
headers:
Content-Transfer-Encoding:
schema:
type: string
example: base64
content:
application/pkcs7-mime:
schema:
type: string
format: byte
description: "Base64-encoded PKCS#7 (smime-type=certs-only)"
"500":
$ref: "#/components/responses/InternalError"
/.well-known/est/simpleenroll:
post:
tags: [EST]
summary: EST simple enrollment
description: |
Enrolls a new certificate from a PKCS#10 CSR per RFC 7030 §4.2.1.
The CSR MAY be supplied as base64-encoded DER (EST standard wire format)
or as PEM for convenience. Returns a base64-encoded PKCS#7 certs-only
structure containing the issued certificate.
operationId: estSimpleEnroll
security: []
requestBody:
required: true
description: "Base64-encoded DER PKCS#10 CSR, or PEM-encoded CSR"
content:
application/pkcs10:
schema:
type: string
format: byte
responses:
"200":
description: Base64-encoded PKCS#7 cert-only response with issued certificate
headers:
Content-Transfer-Encoding:
schema:
type: string
example: base64
content:
application/pkcs7-mime:
schema:
type: string
format: byte
description: "Base64-encoded PKCS#7 (smime-type=certs-only)"
"400":
$ref: "#/components/responses/BadRequest"
"405":
description: Method not allowed (only POST accepted)
"500":
$ref: "#/components/responses/InternalError"
/.well-known/est/simplereenroll:
post:
tags: [EST]
summary: EST simple re-enrollment
description: |
Re-enrolls an existing certificate (same as simpleenroll in certctl's
implementation — re-enrollment is treated as a fresh issuance) per
RFC 7030 §4.2.2.
operationId: estSimpleReEnroll
security: []
requestBody:
required: true
description: "Base64-encoded DER PKCS#10 CSR, or PEM-encoded CSR"
content:
application/pkcs10:
schema:
type: string
format: byte
responses:
"200":
description: Base64-encoded PKCS#7 cert-only response with re-issued certificate
headers:
Content-Transfer-Encoding:
schema:
type: string
example: base64
content:
application/pkcs7-mime:
schema:
type: string
format: byte
description: "Base64-encoded PKCS#7 (smime-type=certs-only)"
"400":
$ref: "#/components/responses/BadRequest"
"405":
description: Method not allowed (only POST accepted)
"500":
$ref: "#/components/responses/InternalError"
/.well-known/est/csrattrs:
get:
tags: [EST]
summary: EST CSR attributes
description: |
Returns attributes the EST client should include in its CSR per
RFC 7030 §4.5. certctl currently returns an empty attribute set
(HTTP 204) — profile-based constraints are enforced server-side
during enrollment rather than advertised here.
operationId: estCSRAttrs
security: []
responses:
"200":
description: Base64-encoded CsrAttrs (when non-empty)
headers:
Content-Transfer-Encoding:
schema:
type: string
example: base64
content:
application/csrattrs:
schema:
type: string
format: byte
"204":
description: No CSR attributes defined (empty response)
"500":
$ref: "#/components/responses/InternalError"
# ─── SCEP (RFC 8894) ──────────────────────────────────────────────
/scep:
get:
tags: [SCEP]
summary: SCEP operation dispatch (GET)
description: |
Single SCEP entry point dispatched by the `operation` query parameter
per RFC 8894. GET is used for capability discovery (`GetCACaps`) and
CA certificate retrieval (`GetCACert`).
operationId: scepGet
security: []
parameters:
- name: operation
in: query
required: true
schema:
type: string
enum: [GetCACaps, GetCACert, PKIOperation]
description: SCEP operation selector
- name: message
in: query
required: false
schema:
type: string
description: Optional SCEP message parameter (base64-encoded for GET PKIOperation)
responses:
"200":
description: |
Success. Content-Type varies by operation:
- `GetCACaps` → `text/plain` capability list
- `GetCACert` (single cert) → `application/x-x509-ca-cert` (raw DER)
- `GetCACert` (chain) → `application/x-x509-ca-ra-cert` (PKCS#7)
- `PKIOperation` → `application/x-pki-message` (PKCS#7 SignedData)
content:
text/plain:
schema:
type: string
description: "SCEP capabilities (GetCACaps only)"
application/x-x509-ca-cert:
schema:
type: string
format: binary
description: "CA certificate DER (GetCACert single)"
application/x-x509-ca-ra-cert:
schema:
type: string
format: binary
description: "CA chain PKCS#7 (GetCACert chain)"
application/x-pki-message:
schema:
type: string
format: binary
description: "PKCS#7 SignedData response (PKIOperation)"
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
post:
tags: [SCEP]
summary: SCEP PKIOperation (POST)
description: |
SCEP enrollment / renewal / revocation request per RFC 8894.
Request body is a PKCS#7 SignedData envelope wrapping the PKCS#10 CSR
or a degenerate raw CSR (fallback). The challenge password in the CSR
attributes is validated against `CERTCTL_SCEP_CHALLENGE_PASSWORD` when
configured.
operationId: scepPost
security: []
parameters:
- name: operation
in: query
required: true
schema:
type: string
enum: [PKIOperation]
requestBody:
required: true
description: PKCS#7 SignedData envelope wrapping a PKCS#10 CSR (or raw CSR as fallback)
content:
application/x-pki-message:
schema:
type: string
format: binary
responses:
"200":
description: PKCS#7 SignedData PKIMessage response
content:
application/x-pki-message:
schema:
type: string
format: binary
"400":
$ref: "#/components/responses/BadRequest"
"500":
$ref: "#/components/responses/InternalError"
# ═══════════════════════════════════════════════════════════════════════
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: API key passed as Bearer token. Configure via CERTCTL_AUTH_SECRET.
parameters:
resourceId:
name: id
in: path
required: true
schema:
type: string
description: Human-readable resource ID (e.g., mc-api-prod, t-platform)
page:
name: page
in: query
schema:
type: integer
default: 1
minimum: 1
per_page:
name: per_page
in: query
schema:
type: integer
default: 50
minimum: 1
maximum: 500
responses:
BadRequest:
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Conflict:
description: Resource conflict
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
InternalError:
description: Internal server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
# ─── Common ──────────────────────────────────────────────────────
ErrorResponse:
type: object
properties:
error:
type: string
request_id:
type: string
StatusResponse:
type: object
properties:
status:
type: string
PaginationEnvelope:
type: object
properties:
total:
type: integer
format: int64
page:
type: integer
per_page:
type: integer
# ─── Certificates ────────────────────────────────────────────────
CertificateStatus:
type: string
enum:
- Pending
- Active
- Expiring
- Expired
- RenewalInProgress
- Failed
- Revoked
- Archived
ManagedCertificate:
type: object
properties:
id:
type: string
name:
type: string
common_name:
type: string
sans:
type: array
items:
type: string
environment:
type: string
owner_id:
type: string
team_id:
type: string
issuer_id:
type: string
target_ids:
type: array
items:
type: string
renewal_policy_id:
type: string
certificate_profile_id:
type: string
status:
$ref: "#/components/schemas/CertificateStatus"
expires_at:
type: string
format: date-time
tags:
type: object
additionalProperties:
type: string
last_renewal_at:
type: string
format: date-time
last_deployment_at:
type: string
format: date-time
revoked_at:
type: string
format: date-time
revocation_reason:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
required:
- name
- common_name
- renewal_policy_id
- issuer_id
- owner_id
- team_id
CertificateVersion:
type: object
properties:
id:
type: string
certificate_id:
type: string
serial_number:
type: string
not_before:
type: string
format: date-time
not_after:
type: string
format: date-time
fingerprint_sha256:
type: string
pem_chain:
type: string
csr_pem:
type: string
key_algorithm:
type: string
key_size:
type: integer
created_at:
type: string
format: date-time
RevocationReason:
type: string
enum:
- unspecified
- keyCompromise
- caCompromise
- affiliationChanged
- superseded
- cessationOfOperation
- certificateHold
- privilegeWithdrawn
BulkRevokeRequest:
type: object
required: [reason]
properties:
reason:
$ref: "#/components/schemas/RevocationReason"
profile_id:
type: string
description: Revoke all certificates matching this profile
owner_id:
type: string
description: Revoke all certificates owned by this owner
agent_id:
type: string
description: Revoke all certificates deployed via this agent
issuer_id:
type: string
description: Revoke all certificates issued by this issuer
team_id:
type: string
description: Revoke all certificates owned by members of this team
certificate_ids:
type: array
items:
type: string
description: Explicit list of certificate IDs to revoke
BulkRevokeResult:
type: object
properties:
total_matched:
type: integer
description: Number of certificates matching the criteria
total_revoked:
type: integer
description: Number of certificates successfully revoked
total_skipped:
type: integer
description: Number of certificates skipped (already revoked or archived)
total_failed:
type: integer
description: Number of certificates that failed to revoke
errors:
type: array
items:
type: object
properties:
certificate_id:
type: string
error:
type: string
description: Per-certificate error details for failed revocations
# ─── Issuers ─────────────────────────────────────────────────────
IssuerType:
type: string
enum: [ACME, GenericCA, StepCA, VaultPKI, DigiCert, Sectigo, GoogleCAS, AWSACMPCA, Entrust, GlobalSign, EJBCA]
Issuer:
type: object
properties:
id:
type: string
name:
type: string
type:
$ref: "#/components/schemas/IssuerType"
config:
type: object
description: Issuer-specific configuration (varies by type)
enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
# ─── Targets ─────────────────────────────────────────────────────
TargetType:
type: string
enum: [NGINX, Apache, HAProxy, Traefik, Caddy, Envoy, Postfix, Dovecot, IIS, F5, SSH, WinCertStore, JavaKeystore, KubernetesSecrets]
DeploymentTarget:
type: object
required: [name, type, agent_id]
properties:
id:
type: string
name:
type: string
type:
$ref: "#/components/schemas/TargetType"
agent_id:
type: string
description: |
ID of the agent that manages this target. Required because
deployment_targets.agent_id is a NOT NULL foreign key to agents(id)
(migration 000001). Empty or nonexistent agent IDs are rejected
with HTTP 400 by the service layer (see C-002 in the coverage-gap
audit).
config:
type: object
description: Target-specific configuration (varies by type)
enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
# ─── Agents ──────────────────────────────────────────────────────
AgentStatus:
type: string
enum: [Online, Offline, Degraded]
Agent:
type: object
properties:
id:
type: string
name:
type: string
hostname:
type: string
status:
$ref: "#/components/schemas/AgentStatus"
last_heartbeat_at:
type: string
format: date-time
registered_at:
type: string
format: date-time
api_key_hash:
type: string
os:
type: string
architecture:
type: string
ip_address:
type: string
version:
type: string
retired_at:
type: string
format: date-time
nullable: true
description: |
I-004: soft-retirement timestamp. `null` (or field absent) means the
agent is active. A non-null value is the canonical "retired" state —
the operational `status` column is preserved at retirement time as
the last-seen value, but `retired_at` is the source of truth for
filtering agents out of active listings.
retired_reason:
type: string
nullable: true
description: |
I-004: human-readable reason captured at retirement time. Only set
when the agent was retired via `?force=true&reason=...` cascade; a
default soft-retire leaves this field null.
AgentDependencyCounts:
type: object
description: |
I-004: preflight counts of active downstream rows that would be
orphaned by retiring an agent. Returned in the 409
`blocked_by_dependencies` body so the operator UI can tell the user
which bucket is blocking the retire, and also in the 200 response
body on a successful `?force=true` cascade as a snapshot of what
was cascaded.
properties:
active_targets:
type: integer
description: Deployment targets with this agent assigned and retired_at IS NULL
active_certificates:
type: integer
description: Certificates currently deployed via one of this agent's active targets
pending_jobs:
type: integer
description: Jobs with agent_id=this in status Pending, AwaitingCSR, AwaitingApproval, or Running
RetireAgentResponse:
type: object
description: |
I-004: response body for a successful retire on DELETE /api/v1/agents/{id}.
Returned on both clean retires (cascade=false, zero counts) and
force-cascade retires (cascade=true, counts snapshot of the
pre-cascade dependency state). The 204 idempotent-retire path does
NOT emit this body — re-retiring an already-retired agent returns
an empty response.
properties:
retired_at:
type: string
format: date-time
already_retired:
type: boolean
description: |
Always false on the 200 response — the already-retired path
returns 204 No Content with no body. Surfaced in the schema
only so downstream consumers have a complete field map.
cascade:
type: boolean
description: True when the retire was invoked with ?force=true
counts:
$ref: "#/components/schemas/AgentDependencyCounts"
BlockedByDependenciesResponse:
type: object
description: |
I-004: 409 response body for a retire request blocked by active
downstream dependencies. Returned when `force=true` is not set and
any of the three counts is non-zero. The operator UI renders these
counts so the human can retire or reassign the blocking rows
before re-running the retire, or tick the force checkbox to cascade.
properties:
error:
type: string
example: blocked_by_dependencies
message:
type: string
counts:
$ref: "#/components/schemas/AgentDependencyCounts"
WorkItem:
type: object
properties:
id:
type: string
type:
$ref: "#/components/schemas/JobType"
certificate_id:
type: string
common_name:
type: string
sans:
type: array
items:
type: string
target_id:
type: string
target_type:
type: string
target_config:
type: object
status:
$ref: "#/components/schemas/JobStatus"
# ─── Jobs ────────────────────────────────────────────────────────
JobType:
type: string
enum: [Issuance, Renewal, Deployment, Validation]
JobStatus:
type: string
enum:
- Pending
- AwaitingCSR
- AwaitingApproval
- Running
- Completed
- Failed
- Cancelled
Job:
type: object
properties:
id:
type: string
type:
$ref: "#/components/schemas/JobType"
certificate_id:
type: string
target_id:
type: string
status:
$ref: "#/components/schemas/JobStatus"
attempts:
type: integer
max_attempts:
type: integer
last_error:
type: string
scheduled_at:
type: string
format: date-time
started_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
created_at:
type: string
format: date-time
# ─── Policies ────────────────────────────────────────────────────
PolicyType:
type: string
enum:
- AllowedIssuers
- AllowedDomains
- RequiredMetadata
- AllowedEnvironments
- RenewalLeadTime
- CertificateLifetime
PolicySeverity:
type: string
enum: [Warning, Error, Critical]
PolicyRule:
type: object
properties:
id:
type: string
name:
type: string
type:
$ref: "#/components/schemas/PolicyType"
config:
type: object
description: Policy-specific configuration (varies by type)
enabled:
type: boolean
severity:
$ref: "#/components/schemas/PolicySeverity"
description: Severity level applied to violations of this rule. Defaults to Warning on create when omitted.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
PolicyViolation:
type: object
properties:
id:
type: string
certificate_id:
type: string
rule_id:
type: string
message:
type: string
severity:
$ref: "#/components/schemas/PolicySeverity"
created_at:
type: string
format: date-time
# ─── Profiles ────────────────────────────────────────────────────
CertificateProfile:
type: object
properties:
id:
type: string
name:
type: string
description:
type: string
allowed_key_algorithms:
type: array
items:
$ref: "#/components/schemas/KeyAlgorithmRule"
max_ttl_seconds:
type: integer
allowed_ekus:
type: array
description: Extended Key Usages to include in issued certificates
items:
type: string
enum:
- serverAuth
- clientAuth
- codeSigning
- emailProtection
- timeStamping
required_san_patterns:
type: array
items:
type: string
spiffe_uri_pattern:
type: string
allow_short_lived:
type: boolean
enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
KeyAlgorithmRule:
type: object
properties:
algorithm:
type: string
enum: [RSA, ECDSA, Ed25519]
min_size:
type: integer
# ─── Teams ───────────────────────────────────────────────────────
Team:
type: object
properties:
id:
type: string
name:
type: string
description:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
# ─── Owners ──────────────────────────────────────────────────────
Owner:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
team_id:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
# ─── Agent Groups ────────────────────────────────────────────────
AgentGroup:
type: object
properties:
id:
type: string
name:
type: string
description:
type: string
match_os:
type: string
match_architecture:
type: string
match_ip_cidr:
type: string
match_version:
type: string
enabled:
type: boolean
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
# ─── Audit ───────────────────────────────────────────────────────
ActorType:
type: string
enum: [User, System, Agent]
AuditEvent:
type: object
properties:
id:
type: string
actor:
type: string
actor_type:
$ref: "#/components/schemas/ActorType"
action:
type: string
resource_type:
type: string
resource_id:
type: string
details:
type: object
timestamp:
type: string
format: date-time
# ─── Notifications ───────────────────────────────────────────────
NotificationType:
type: string
enum:
- ExpirationWarning
- RenewalSuccess
- RenewalFailure
- DeploymentSuccess
- DeploymentFailure
- PolicyViolation
- Revocation
NotificationChannel:
type: string
enum: [Email, Webhook, Slack]
NotificationEvent:
type: object
properties:
id:
type: string
type:
$ref: "#/components/schemas/NotificationType"
certificate_id:
type: string
channel:
$ref: "#/components/schemas/NotificationChannel"
recipient:
type: string
message:
type: string
sent_at:
type: string
format: date-time
status:
type: string
error:
type: string
created_at:
type: string
format: date-time
# ─── Stats & Metrics ─────────────────────────────────────────────
DashboardSummary:
type: object
properties:
total_certificates:
type: integer
format: int64
expiring_certificates:
type: integer
format: int64
expired_certificates:
type: integer
format: int64
revoked_certificates:
type: integer
format: int64
active_agents:
type: integer
format: int64
offline_agents:
type: integer
format: int64
total_agents:
type: integer
format: int64
pending_jobs:
type: integer
format: int64
failed_jobs:
type: integer
format: int64
complete_jobs:
type: integer
format: int64
completed_at:
type: string
format: date-time
MetricsResponse:
type: object
properties:
gauge:
type: object
properties:
certificate_total:
type: integer
format: int64
certificate_active:
type: integer
format: int64
certificate_expiring_soon:
type: integer
format: int64
certificate_expired:
type: integer
format: int64
certificate_revoked:
type: integer
format: int64
agent_total:
type: integer
format: int64
agent_online:
type: integer
format: int64
job_pending:
type: integer
format: int64
counter:
type: object
properties:
job_completed_total:
type: integer
format: int64
job_failed_total:
type: integer
format: int64
uptime:
type: object
properties:
uptime_seconds:
type: integer
format: int64
server_started:
type: string
format: date-time
measured_at:
type: string
format: date-time
# ─── Discovery (M18b) ────────────────────────────────────────────
DiscoveredCertificate:
type: object
properties:
id:
type: string
fingerprint_sha256:
type: string
common_name:
type: string
sans:
type: array
items:
type: string
serial_number:
type: string
issuer_dn:
type: string
subject_dn:
type: string
not_before:
type: string
format: date-time
nullable: true
not_after:
type: string
format: date-time
nullable: true
key_algorithm:
type: string
key_size:
type: integer
is_ca:
type: boolean
source_path:
type: string
source_format:
type: string
agent_id:
type: string
discovery_scan_id:
type: string
nullable: true
managed_certificate_id:
type: string
nullable: true
status:
type: string
enum: [Unmanaged, Managed, Dismissed]
first_seen_at:
type: string
format: date-time
last_seen_at:
type: string
format: date-time
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
DiscoveryScan:
type: object
properties:
id:
type: string
agent_id:
type: string
directories:
type: array
items:
type: string
certificates_found:
type: integer
certificates_new:
type: integer
errors_count:
type: integer
scan_duration_ms:
type: integer
started_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
nullable: true
DiscoveryReport:
type: object
required: [agent_id, directories, certificates]
properties:
agent_id:
type: string
directories:
type: array
items:
type: string
certificates:
type: array
items:
type: object
properties:
fingerprint_sha256:
type: string
common_name:
type: string
sans:
type: array
items:
type: string
serial_number:
type: string
issuer_dn:
type: string
subject_dn:
type: string
not_before:
type: string
not_after:
type: string
key_algorithm:
type: string
key_size:
type: integer
is_ca:
type: boolean
pem_data:
type: string
source_path:
type: string
source_format:
type: string
errors:
type: array
items:
type: string
scan_duration_ms:
type: integer
StatusMessageResponse:
type: object
properties:
status:
type: string
message:
type: string
# ─── Network Scan (M21) ──────────────────────────────────────────
NetworkScanTarget:
type: object
properties:
id:
type: string
name:
type: string
cidrs:
type: array
items:
type: string
description: CIDR ranges to scan (max /20 per CIDR)
ports:
type: array
items:
type: integer
description: TCP ports to probe for TLS
enabled:
type: boolean
scan_interval_hours:
type: integer
description: Hours between scheduled scans
timeout_ms:
type: integer
description: Per-connection timeout in milliseconds
last_scan_at:
type: string
format: date-time
nullable: true
last_scan_duration_ms:
type: integer
nullable: true
last_scan_certs_found:
type: integer
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
NetworkScanTargetCreate:
type: object
required: [name, cidrs]
properties:
name:
type: string
cidrs:
type: array
items:
type: string
description: CIDR ranges (max /20 per CIDR, max 4096 IPs)
ports:
type: array
items:
type: integer
description: TCP ports to probe (default [443])
enabled:
type: boolean
default: true
scan_interval_hours:
type: integer
default: 6
timeout_ms:
type: integer
default: 5000
EndpointHealthCheck:
type: object
properties:
id:
type: string
description: Health check ID
endpoint:
type: string
description: "Target endpoint (host:port)"
example: "api.example.com:443"
certificate_id:
type: string
nullable: true
description: Associated managed certificate ID (if from deployment)
network_scan_target_id:
type: string
nullable: true
description: Associated network scan target ID (if auto-created)
expected_fingerprint:
type: string
nullable: true
description: Expected certificate SHA-256 fingerprint
status:
type: string
enum: [Healthy, Degraded, Down, CertMismatch]
description: Current health status
enabled:
type: boolean
check_interval_seconds:
type: integer
description: Frequency of TLS probes (seconds)
timeout_ms:
type: integer
description: TLS connection timeout (milliseconds)
consecutive_failures:
type: integer
description: Number of consecutive probe failures
last_checked_at:
type: string
format: date-time
nullable: true
description: Timestamp of last probe
last_success_at:
type: string
format: date-time
nullable: true
description: Timestamp of last successful probe
last_failure_at:
type: string
format: date-time
nullable: true
description: Timestamp of last failed probe
last_transition_at:
type: string
format: date-time
nullable: true
description: Timestamp of last status transition
failure_reason:
type: string
nullable: true
description: Reason for last failure
acknowledged:
type: boolean
description: Whether the current status has been acknowledged
acknowledged_by:
type: string
nullable: true
description: Operator name who acknowledged (if applicable)
acknowledged_at:
type: string
format: date-time
nullable: true
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
HealthHistoryEntry:
type: object
properties:
id:
type: string
health_check_id:
type: string
status:
type: string
enum: [Healthy, Degraded, Down, CertMismatch]
response_time_ms:
type: integer
nullable: true
description: Time to connect and complete TLS handshake (milliseconds)
observed_fingerprint:
type: string
nullable: true
description: SHA-256 fingerprint of certificate observed on endpoint
tls_version:
type: string
nullable: true
description: TLS version (e.g., TLSv1.3)
cipher_suite:
type: string
nullable: true
description: Cipher suite used in TLS handshake
cert_subject:
type: string
nullable: true
description: Subject DN of observed certificate
cert_issuer:
type: string
nullable: true
description: Issuer DN of observed certificate
cert_not_before:
type: string
format: date-time
nullable: true
cert_not_after:
type: string
format: date-time
nullable: true
failure_reason:
type: string
nullable: true
description: Error message if probe failed
checked_at:
type: string
format: date-time
description: Timestamp of this probe
# ─── Verification (M25) ──────────────────────────────────────────
VerifyDeploymentRequest:
type: object
required: [target_id, expected_fingerprint, actual_fingerprint, verified]
properties:
target_id:
type: string
description: Deployment target the agent probed
expected_fingerprint:
type: string
description: SHA-256 fingerprint of the certificate that should be served (hex, lowercase)
actual_fingerprint:
type: string
description: SHA-256 fingerprint observed on the live TLS endpoint (hex, lowercase)
verified:
type: boolean
description: True when expected and actual fingerprints match
error:
type: string
nullable: true
description: Error message when probe failed or fingerprints differ
VerificationResult:
type: object
properties:
job_id:
type: string
target_id:
type: string
expected_fingerprint:
type: string
description: SHA-256 fingerprint (hex) of the certificate deployed by this job
actual_fingerprint:
type: string
description: SHA-256 fingerprint (hex) observed on the live TLS endpoint
verified:
type: boolean
verified_at:
type: string
format: date-time
error:
type: string
description: Error message when verification failed