mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:51:33 +00:00
3287e174dc
Closes the remaining P1 gaps from coverage-gap-audit.md (M-001/M-002/M-003/M-006)
on top of the C-001/C-002 ownership + agent-FK contract fixes landed in
a53a4b8. The work lands as a single commit spanning server, docs, tests,
and the React client.
M-002 — Named API keys with per-key actor propagation
* Migration 000014 adds the 'api_keys' table (id, name, hash,
principal, role, created_at, last_used_at, disabled_at) so every
credential carries an identifiable principal instead of the
opaque 'anonymous'/'api-key' sentinel.
* Auth middleware now rotates through configured keys, performs
constant-time hash comparison, stamps 'last_used_at', and emits
an actor struct via contextWithActor(). The audit middleware,
bulk-revocation handler, approval handlers, and MCP tool layer
now read the principal off the context and persist it on every
audit_events row.
* Regression coverage:
- internal/api/middleware/audit_test.go — actor propagation,
principal redaction for disabled keys, anonymous fallback for
unauthenticated endpoints.
- internal/api/handler/bulk_revocation_handler_test.go,
job_handler_test.go — principal-on-audit assertions.
M-003 — Authorization gates (Phase B)
* Approval handler rejects self-approval / self-rejection with 403
when the actor principal equals the job's requested_by field.
* Bulk revocation is gated behind the 'admin' role; operators and
viewers receive 403.
* Regression coverage:
- internal/service/job_test.go — TestApproveJob_NotSelf,
TestRejectJob_NotSelf.
- internal/api/handler/bulk_revocation_handler_test.go —
TestBulkRevoke_RequiresAdmin, TestBulkRevoke_AdminSucceeds.
M-006 — RFC-compliant CRL/OCSP on the unauthenticated .well-known mux
* Per RFC 8615, relying parties cannot reasonably be asked to
authenticate against the issuing certctl instance to retrieve
revocation material. CRL and OCSP move off the authenticated
'/api/v1/crl*' and '/api/v1/ocsp/*' paths onto:
GET /.well-known/pki/crl/{issuer_id}
Content-Type: application/pkix-crl (RFC 5280 §5)
GET /.well-known/pki/ocsp/{issuer_id}/{serial}
Content-Type: application/ocsp-response (RFC 6960)
* Non-standard JSON CRL shape is removed; only DER is served.
* Short-lived certificate exemption (profile TTL < 1h → skip
CRL/OCSP) is preserved; the response simply omits the serial.
* Routes are registered on the unauthenticated 'finalHandler' mux
in cmd/server/main.go alongside EST ('/.well-known/est/*') and
SCEP ('/scep'). Legacy authenticated paths return 404.
* Regression coverage:
- internal/api/handler/certificate_handler_test.go — content
type, DER parseability, 404 for unknown issuer.
- internal/api/handler/adversarial_path_test.go — unauthenticated
access asserted for CRL, OCSP, EST, SCEP.
- internal/api/router/router_test.go — route-table assertion
that '.well-known/pki/*', '.well-known/est/*', and '/scep' are
mounted on the unauthenticated branch.
M-001 — Auto-closed by M-002
EST and SCEP were already registered on the unauthenticated
'finalHandler' mux; the router comment at
internal/api/router/router.go:247 now matches reality. The
adversarial-path tests above lock the behavior in.
Verification (all gates green):
* go vet ./... — clean
* go build ./... — ok
* go test -short ./... (55+ packages) — all pass
* web/ : npm test (225 Vitest tests) — all pass
* web/ : npx tsc --noEmit — clean
* grep sweep for '/api/v1/(crl|ocsp)' — 13 surviving hits,
all intentional M-006 tombstone/relocation comments.
Documentation:
* coverage-gap-audit.md — status flips M-001/M-002/M-003/M-006 →
Fixed, with per-finding resolution paragraphs citing regression
test IDs. (Audit file lives outside this repo; see cowork root.)
* CLAUDE.md Project Status line updated with the auth-unification
closure note.
* docs/features.md, docs/architecture.md, docs/quickstart.md,
docs/concepts.md, docs/connectors.md, docs/test-env.md,
docs/testing-guide.md, docs/compliance-*.md, docs/demo-advanced.md
— refreshed for the new '.well-known/pki/*' namespace and named
API keys.
* api/openapi.yaml — documents the new unauthenticated endpoints
and removes the legacy '/api/v1/crl*' + '/api/v1/ocsp/*' paths.
.gitignore: adds '/.gocache/' and '/.gomodcache/' for the session-
scoped Go caches so they never enter the tree.
4170 lines
126 KiB
YAML
4170 lines
126 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/{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"
|
|
|
|
/api/v1/agents/{id}/heartbeat:
|
|
post:
|
|
tags: [Agents]
|
|
summary: Agent heartbeat
|
|
description: Reports agent liveness and metadata (OS, architecture, IP, version).
|
|
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"
|
|
"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
|
|
|
|
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
|