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: https://localhost:8443 description: Docker Compose demo (self-signed cert; pin with ./deploy/test/certs/ca.crt) 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: RenewalPolicies description: Lifecycle renewal policies (distinct from compliance policy rules above) - 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 # G-1 (P1): "jwt" removed from this enum after the silent # auth downgrade was identified — no JWT middleware ships # with certctl. Operators who need JWT/OIDC front certctl # with an authenticating gateway (oauth2-proxy / Envoy / # Traefik / Pomerium) and set CERTCTL_AUTH_TYPE=none # upstream. See docs/architecture.md "Authenticating- # gateway pattern". enum: [api-key, 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 /api/v1/version: get: tags: [Health] summary: Build identity (version, commit, Go runtime) description: | Returns the running server's build identity. Served without auth so rollout systems and blackbox probes can read it without Bearer credentials. U-3 ride-along (cat-u-no_version_endpoint). Excluded from audit logging because rollout polling would otherwise dominate the audit trail. The Version field follows a fallback ladder: ldflags-supplied value > VCS commit SHA > "dev". Commit / Modified / BuildTime come from runtime/debug.BuildInfo (Go 1.18+ stamps these on every module-tracked build). GoVersion is runtime.Version(). security: [] operationId: getVersion responses: "200": description: Build identity content: application/json: schema: type: object required: [version, commit, modified, build_time, go_version] properties: version: type: string description: Release tag (ldflags-supplied) or VCS SHA fallback or "dev" example: v2.0.51 commit: type: string description: Git SHA from runtime/debug.BuildInfo (vcs.revision); empty when not VCS-tracked modified: type: boolean description: True when build had uncommitted changes (vcs.modified) build_time: type: string description: RFC 3339 build timestamp (vcs.time); empty when not VCS-tracked go_version: type: string description: Go toolchain version that compiled the binary (runtime.Version()) example: go1.25.9 # ─── Certificates ──────────────────────────────────────────────────── /api/v1/certificates: get: tags: [Certificates] summary: List certificates operationId: listCertificates parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - name: status in: query schema: $ref: "#/components/schemas/CertificateStatus" - name: environment in: query schema: type: string - name: owner_id in: query schema: type: string - name: team_id in: query schema: type: string - name: issuer_id in: query schema: type: string responses: "200": description: Paginated list of certificates content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/ManagedCertificate" "500": $ref: "#/components/responses/InternalError" post: tags: [Certificates] summary: Create certificate operationId: createCertificate requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ManagedCertificate" responses: "201": description: Certificate created content: application/json: schema: $ref: "#/components/schemas/ManagedCertificate" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}: get: tags: [Certificates] summary: Get certificate operationId: getCertificate parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Certificate details content: application/json: schema: $ref: "#/components/schemas/ManagedCertificate" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [Certificates] summary: Update certificate operationId: updateCertificate parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ManagedCertificate" responses: "200": description: Certificate updated content: application/json: schema: $ref: "#/components/schemas/ManagedCertificate" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: tags: [Certificates] summary: Archive certificate operationId: archiveCertificate parameters: - $ref: "#/components/parameters/resourceId" responses: "204": description: Certificate archived "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}/versions: get: tags: [Certificates] summary: List certificate versions operationId: listCertificateVersions parameters: - $ref: "#/components/parameters/resourceId" - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of certificate versions content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/CertificateVersion" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}/renew: post: tags: [Certificates] summary: Trigger certificate renewal operationId: triggerRenewal parameters: - $ref: "#/components/parameters/resourceId" responses: "202": description: Renewal triggered content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}/deploy: post: tags: [Certificates] summary: Trigger certificate deployment operationId: triggerDeployment parameters: - $ref: "#/components/parameters/resourceId" requestBody: content: application/json: schema: type: object properties: target_id: type: string description: Optional specific target ID responses: "202": description: Deployment triggered content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}/revoke: post: tags: [Certificates] summary: Revoke certificate description: | Revokes a certificate with an optional RFC 5280 reason code. Records revocation in cert inventory, audit log, and certificate_revocations table. Best-effort issuer notification. operationId: revokeCertificate parameters: - $ref: "#/components/parameters/resourceId" requestBody: content: application/json: schema: type: object properties: reason: $ref: "#/components/schemas/RevocationReason" responses: "200": description: Certificate revoked content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" # ─── Bulk Revocation ───────────────────────────────────────────────── /api/v1/certificates/bulk-revoke: post: tags: [Certificates] summary: Bulk revoke certificates description: | Revokes all certificates matching the given filter criteria. At least one criterion is required (safety guard against accidental mass revocation). Reuses the single-cert revocation flow per certificate with partial-failure tolerance. operationId: bulkRevokeCertificates requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BulkRevokeRequest" responses: "200": description: Bulk revocation result content: application/json: schema: $ref: "#/components/schemas/BulkRevokeResult" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" # ─── Certificate Export ────────────────────────────────────────────── /api/v1/certificates/{id}/export/pem: get: tags: [Certificates] summary: Export certificate as PEM description: | Returns the certificate and its chain in PEM format. By default returns JSON with cert_pem, chain_pem, and full_pem fields. Add ?download=true to get the full PEM chain as a file download with Content-Disposition headers. operationId: exportCertificatePEM parameters: - $ref: "#/components/parameters/resourceId" - name: download in: query schema: type: string enum: ["true"] description: Set to "true" to get a file download instead of JSON. responses: "200": description: PEM export content: application/json: schema: type: object properties: cert_pem: type: string description: Leaf certificate PEM chain_pem: type: string description: Intermediate/root chain PEM full_pem: type: string description: Full PEM chain (cert + intermediates) application/x-pem-file: schema: type: string format: binary description: Full PEM file (when download=true) "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/certificates/{id}/export/pkcs12: post: tags: [Certificates] summary: Export certificate as PKCS#12 description: | Returns a PKCS#12 (.p12) bundle containing the certificate and chain. Private keys are NOT included — they live on agents and never touch the control plane. The bundle is encrypted with the provided password (or empty password if omitted). operationId: exportCertificatePKCS12 parameters: - $ref: "#/components/parameters/resourceId" requestBody: content: application/json: schema: type: object properties: password: type: string description: Password to encrypt the PKCS#12 bundle (can be empty) responses: "200": description: PKCS#12 binary content: application/x-pkcs12: schema: type: string format: binary "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" # ─── PKI (CRL & OCSP, RFC 5280 / 6960 / 8615) ────────────────────── # # Relying parties (browsers, OpenSSL clients, OCSP stapling sidecars, # mTLS clients) cannot present a certctl Bearer token, so these two # endpoints are unauthenticated and live under the RFC 8615 # `.well-known` namespace. They were previously mounted at # /api/v1/crl/{issuer_id} and /api/v1/ocsp/{issuer_id}/{serial}; those # paths were removed in M-006. # # The non-standard JSON CRL endpoint (GET /api/v1/crl) was also # removed — RFC 5280 defines only the DER wire format. /.well-known/pki/crl/{issuer_id}: get: tags: [CRL & OCSP] summary: Get DER-encoded X.509 CRL (RFC 5280) description: | Returns a DER-encoded CRL signed by the issuing CA (RFC 5280 §5), served unauthenticated per RFC 8615 `.well-known` semantics so relying parties can retrieve it without a certctl API key. Validity is 24 hours. operationId: getDERCRL security: [] parameters: - name: issuer_id in: path required: true schema: type: string responses: "200": description: DER-encoded CRL content: application/pkix-crl: schema: type: string format: binary "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "501": description: Issuer does not support CRL generation /.well-known/pki/ocsp/{issuer_id}/{serial}: get: tags: [CRL & OCSP] summary: OCSP responder (RFC 6960) description: | Returns a signed OCSP response (good/revoked/unknown) for the given serial number per RFC 6960 §2.1, served unauthenticated per RFC 8615 so relying parties and OCSP stapling sidecars can query revocation status without a certctl API key. operationId: handleOCSP security: [] parameters: - name: issuer_id in: path required: true schema: type: string - name: serial in: path required: true description: Hex-encoded certificate serial number schema: type: string responses: "200": description: OCSP response content: application/ocsp-response: schema: type: string format: binary "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "501": description: Issuer does not support OCSP # ─── Issuers ───────────────────────────────────────────────────────── /api/v1/issuers: get: tags: [Issuers] summary: List issuers operationId: listIssuers parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of issuers content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Issuer" "500": $ref: "#/components/responses/InternalError" post: tags: [Issuers] summary: Create issuer operationId: createIssuer requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Issuer" responses: "201": description: Issuer created content: application/json: schema: $ref: "#/components/schemas/Issuer" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/issuers/{id}: get: tags: [Issuers] summary: Get issuer operationId: getIssuer parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Issuer details content: application/json: schema: $ref: "#/components/schemas/Issuer" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [Issuers] summary: Update issuer operationId: updateIssuer parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Issuer" responses: "200": description: Issuer updated content: application/json: schema: $ref: "#/components/schemas/Issuer" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" delete: tags: [Issuers] summary: Delete issuer operationId: deleteIssuer parameters: - $ref: "#/components/parameters/resourceId" responses: "204": description: Issuer deleted "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/issuers/{id}/test: post: tags: [Issuers] summary: Test issuer connection operationId: testIssuerConnection parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Connection successful content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" # ─── Targets ───────────────────────────────────────────────────────── /api/v1/targets: get: tags: [Targets] summary: List targets operationId: listTargets parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of targets content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/DeploymentTarget" "500": $ref: "#/components/responses/InternalError" post: tags: [Targets] summary: Create target operationId: createTarget requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeploymentTarget" responses: "201": description: Target created content: application/json: schema: $ref: "#/components/schemas/DeploymentTarget" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/targets/{id}: get: tags: [Targets] summary: Get target operationId: getTarget parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Target details content: application/json: schema: $ref: "#/components/schemas/DeploymentTarget" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [Targets] summary: Update target operationId: updateTarget parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeploymentTarget" responses: "200": description: Target updated content: application/json: schema: $ref: "#/components/schemas/DeploymentTarget" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" delete: tags: [Targets] summary: Delete target operationId: deleteTarget parameters: - $ref: "#/components/parameters/resourceId" responses: "204": description: Target deleted "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/targets/{id}/test: post: tags: [Targets] summary: Test target connection description: | Checks target connectivity by verifying the assigned agent's heartbeat status (agent reported within the last 5 minutes). Always returns HTTP 200 — the connectivity result is reflected in the response body's `status` field (`success` when the agent is reachable, `failed` otherwise). operationId: testTargetConnection parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Connection test result (success or failed in body) content: application/json: schema: $ref: "#/components/schemas/StatusMessageResponse" "400": $ref: "#/components/responses/BadRequest" # ─── Agents ────────────────────────────────────────────────────────── /api/v1/agents: get: tags: [Agents] summary: List agents operationId: listAgents parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of agents content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Agent" "500": $ref: "#/components/responses/InternalError" post: tags: [Agents] summary: Register agent operationId: registerAgent requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Agent" responses: "201": description: Agent registered content: application/json: schema: $ref: "#/components/schemas/Agent" "400": $ref: "#/components/responses/BadRequest" "409": $ref: "#/components/responses/Conflict" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/retired: get: tags: [Agents] summary: List retired agents description: | I-004: opt-in listing of soft-retired agents. The default `GET /api/v1/agents` endpoint filters retired rows out; this is the dedicated surface for reading them back (e.g., the operator UI's "Retired" tab, audit and forensics workflows). Pagination defaults match the default agent listing (page=1, per_page=50, max 500). Go 1.22's enhanced ServeMux routes `/agents/retired` to this handler via the literal-beats-pattern-var precedence rule, so the sibling `/agents/{id}` route does not shadow it. operationId: listRetiredAgents parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of retired agents content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Agent" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}: get: tags: [Agents] summary: Get agent operationId: getAgent parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Agent details content: application/json: schema: $ref: "#/components/schemas/Agent" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: tags: [Agents] summary: Soft-retire agent description: | I-004: soft-retirement. The agent row is preserved (so its audit trail and historical job links remain intact) and `retired_at` is stamped. A retired agent receives `410 Gone` on subsequent heartbeats so it can shut down cleanly. Behavior matrix: | Scenario | Query | Status | Body | | --- | --- | --- | --- | | Clean retire (no active dependencies) | none | `200` | `RetireAgentResponse` with `cascade=false`, zero counts | | Blocked by active targets/certs/jobs | none | `409` | `BlockedByDependenciesResponse` with per-bucket counts | | Force-cascade retire | `force=true&reason=...` | `200` | `RetireAgentResponse` with `cascade=true`, pre-cascade counts | | Idempotent re-retire | either | `204` | (empty — downstream consumers break on stray bodies) | | `force=true` without reason | `force=true` | `400` | ErrorResponse (ErrForceReasonRequired) | | Reserved sentinel agent | any | `403` | ErrorResponse (ErrAgentIsSentinel) | | Unknown agent id | any | `404` | ErrorResponse | Sentinel agents are the four reserved identities backing non-agent discovery subsystems (`server-scanner`, `cloud-aws-sm`, `cloud-azure-kv`, `cloud-gcp-sm`). Retiring them would orphan the scanner or a cloud secret-manager source, so the handler refuses unconditionally — even with `force=true`. operationId: retireAgent parameters: - $ref: "#/components/parameters/resourceId" - name: force in: query required: false schema: type: boolean default: false description: | Cascade-retire active downstream targets, certificates, and jobs. When `true`, a non-empty `reason` is required. A malformed value (anything strconv.ParseBool rejects) is silently treated as `false` so a typoed query can never accidentally enable the cascade. - name: reason in: query required: false schema: type: string description: | Human-readable reason recorded on the retired row and in the immutable audit trail. Required (non-empty after trimming) when `force=true`. responses: "200": description: | Agent retired (clean retire or successful force-cascade). Body is `RetireAgentResponse`. content: application/json: schema: $ref: "#/components/schemas/RetireAgentResponse" "204": description: | Idempotent retire — the agent was already retired. Response body is empty (the 200-path shape does not apply, and downstream clients that tee responses into dashboards would break on spurious bodies). "400": description: | `force=true` was sent without a non-empty `reason` (ErrForceReasonRequired). content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "403": description: | Agent is a reserved sentinel and cannot be retired even with `?force=true` (ErrAgentIsSentinel). content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "404": $ref: "#/components/responses/NotFound" "409": description: | Blocked by active downstream dependencies. Body carries per-bucket counts so the operator UI can show the user which dependency is holding up the retire. Re-run with `?force=true&reason=...` to cascade. content: application/json: schema: $ref: "#/components/schemas/BlockedByDependenciesResponse" "405": description: Method not allowed (only DELETE, GET are routed to this path) "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}/heartbeat: post: tags: [Agents] summary: Agent heartbeat description: | Reports agent liveness and metadata (OS, architecture, IP, version). I-004: a retired agent still polling the heartbeat endpoint receives `410 Gone` so `cmd/agent` detects the terminal signal and shuts down cleanly instead of looping forever against a decommissioned identity. The retired-agent check runs before any "not found" string match so it can never be masked by a sibling error branch. operationId: agentHeartbeat parameters: - $ref: "#/components/parameters/resourceId" requestBody: content: application/json: schema: type: object properties: version: type: string hostname: type: string os: type: string architecture: type: string ip_address: type: string responses: "200": description: Heartbeat recorded content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "410": description: | I-004: the agent has been soft-retired. The agent process should treat this as a terminal signal and shut down cleanly. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}/csr: post: tags: [Agents] summary: Submit CSR description: Agent submits a PEM-encoded CSR for signing. Used in agent keygen mode. operationId: agentSubmitCSR parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: type: object required: [csr_pem] properties: csr_pem: type: string description: PEM-encoded certificate signing request certificate_id: type: string responses: "202": description: CSR accepted content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}/certificates/{cert_id}: get: tags: [Agents] summary: Pick up signed certificate description: Agent retrieves the signed certificate PEM after CSR signing completes. operationId: agentPickupCertificate parameters: - $ref: "#/components/parameters/resourceId" - name: cert_id in: path required: true schema: type: string responses: "200": description: Certificate PEM content: application/json: schema: type: object properties: certificate_pem: type: string "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}/work: get: tags: [Agents] summary: Get pending work description: Returns pending deployment and AwaitingCSR jobs for the agent. operationId: agentGetWork parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Work items content: application/json: schema: type: object properties: jobs: type: array items: $ref: "#/components/schemas/WorkItem" count: type: integer "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/agents/{id}/jobs/{job_id}/status: post: tags: [Agents] summary: Report job status description: Agent reports completion or failure of an assigned job. operationId: agentReportJobStatus parameters: - $ref: "#/components/parameters/resourceId" - name: job_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: type: string error: type: string responses: "200": description: Status updated content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" # ─── Jobs ──────────────────────────────────────────────────────────── /api/v1/jobs: get: tags: [Jobs] summary: List jobs operationId: listJobs parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" - name: status in: query schema: $ref: "#/components/schemas/JobStatus" - name: type in: query schema: $ref: "#/components/schemas/JobType" responses: "200": description: Paginated list of jobs content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/Job" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}: get: tags: [Jobs] summary: Get job operationId: getJob parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Job details content: application/json: schema: $ref: "#/components/schemas/Job" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}/cancel: post: tags: [Jobs] summary: Cancel job operationId: cancelJob parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Job cancelled content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}/approve: post: tags: [Jobs] summary: Approve job description: Approves a job in AwaitingApproval state. operationId: approveJob parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Job approved content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}/reject: post: tags: [Jobs] summary: Reject job description: Rejects a job in AwaitingApproval state with an optional reason. operationId: rejectJob parameters: - $ref: "#/components/parameters/resourceId" requestBody: content: application/json: schema: type: object properties: reason: type: string responses: "200": description: Job rejected content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}/verify: post: tags: [Verification] summary: Record post-deployment verification result description: | Agents submit the result of probing a deployed certificate's live TLS endpoint. Compares the served certificate's SHA-256 fingerprint against the expected fingerprint. Best-effort: failures are recorded on the job but do not roll back the deployment. operationId: verifyDeployment parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/VerifyDeploymentRequest" responses: "200": description: Verification result recorded content: application/json: schema: type: object properties: job_id: type: string verified: type: boolean verified_at: type: string format: date-time "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/jobs/{id}/verification: get: tags: [Verification] summary: Get post-deployment verification status description: | Returns the stored verification result for a deployment job — expected and observed SHA-256 fingerprints, verified flag, and timestamp. operationId: getJobVerification parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Verification result for the job content: application/json: schema: $ref: "#/components/schemas/VerificationResult" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" # ─── Policies ──────────────────────────────────────────────────────── /api/v1/policies: get: tags: [Policies] summary: List policies operationId: listPolicies parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of policies content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/PolicyRule" "500": $ref: "#/components/responses/InternalError" post: tags: [Policies] summary: Create policy operationId: createPolicy requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PolicyRule" responses: "201": description: Policy created content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/policies/{id}: get: tags: [Policies] summary: Get policy operationId: getPolicy parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Policy details content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [Policies] summary: Update policy operationId: updatePolicy parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PolicyRule" responses: "200": description: Policy updated content: application/json: schema: $ref: "#/components/schemas/PolicyRule" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" delete: tags: [Policies] summary: Delete policy operationId: deletePolicy parameters: - $ref: "#/components/parameters/resourceId" responses: "204": description: Policy deleted "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" /api/v1/policies/{id}/violations: get: tags: [Policies] summary: List policy violations operationId: listPolicyViolations parameters: - $ref: "#/components/parameters/resourceId" - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of violations content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/PolicyViolation" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" # ─── Renewal Policies ──────────────────────────────────────────────── # G-1: lifecycle policies (rp-* ids, table renewal_policies). DISTINCT from # /api/v1/policies above, which returns compliance rules (pol-* ids, table # policy_rules). `managed_certificates.renewal_policy_id` FK points at # renewal_policies(id) — populating that dropdown from /api/v1/policies # caused 23503 FK violations; hence this endpoint. /api/v1/renewal-policies: get: tags: [RenewalPolicies] summary: List renewal policies operationId: listRenewalPolicies parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/per_page" responses: "200": description: Paginated list of renewal policies content: application/json: schema: allOf: - $ref: "#/components/schemas/PaginationEnvelope" - type: object properties: data: type: array items: $ref: "#/components/schemas/RenewalPolicy" "500": $ref: "#/components/responses/InternalError" post: tags: [RenewalPolicies] summary: Create renewal policy operationId: createRenewalPolicy requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenewalPolicyCreateRequest" responses: "201": description: Renewal policy created content: application/json: schema: $ref: "#/components/schemas/RenewalPolicy" "400": $ref: "#/components/responses/BadRequest" "409": description: Duplicate policy name content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/v1/renewal-policies/{id}: get: tags: [RenewalPolicies] summary: Get renewal policy operationId: getRenewalPolicy parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Renewal policy details content: application/json: schema: $ref: "#/components/schemas/RenewalPolicy" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: tags: [RenewalPolicies] summary: Update renewal policy operationId: updateRenewalPolicy parameters: - $ref: "#/components/parameters/resourceId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RenewalPolicyUpdateRequest" responses: "200": description: Renewal policy updated content: application/json: schema: $ref: "#/components/schemas/RenewalPolicy" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "409": description: Duplicate policy name content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" delete: tags: [RenewalPolicies] summary: Delete renewal policy operationId: deleteRenewalPolicy parameters: - $ref: "#/components/parameters/resourceId" responses: "204": description: Renewal policy deleted "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "409": description: Policy in use by one or more certificates (FK restrict) content: application/json: schema: $ref: "#/components/schemas/Error" "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" - name: status in: query required: false description: | Filter by lifecycle status. I-005: `dead` powers the Dead letter tab on the GUI; empty/omitted returns the default all-statuses listing to preserve pre-I-005 behavior. schema: type: string enum: [pending, sent, failed, dead, read] 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" /api/v1/notifications/{id}/requeue: post: tags: [Notifications] summary: Requeue a dead notification description: | I-005: flip a notification from the `dead` dead-letter queue back to `pending` so the retry sweep (default 2 minutes) picks it up on its next tick. Used by operators after fixing the underlying delivery failure (SMTP config, webhook endpoint, etc.). Clears `next_retry_at` and resets the `retry_count` budget; `last_error` is preserved for audit continuity. operationId: requeueNotification parameters: - $ref: "#/components/parameters/resourceId" responses: "200": description: Requeued content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" "405": description: Method not allowed (POST only) "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: "..." "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 # G-2 (P1): the `api_key_hash` field was REMOVED from this # schema after cat-s5-apikey_leak audit closure. The DB column # still exists (migrations/000001_initial_schema.up.sql) and # the server still populates the in-memory struct for the # auth-lookup path (repository.AgentRepository::GetByAPIKey), # but the JSON wire shape no longer carries it — see # internal/domain/connector.go::Agent::APIKeyHash + MarshalJSON # for the redaction enforcement and docs/architecture.md ER # diagram for the database-vs-API distinction. Do NOT re-add # the field here without first removing the JSON-shape redaction # in the domain package; the CI guardrail at # .github/workflows/ci.yml will block re-introduction either way. os: type: string architecture: type: string ip_address: type: string version: type: string retired_at: type: string format: date-time nullable: true description: | I-004: soft-retirement timestamp. `null` (or field absent) means the agent is active. A non-null value is the canonical "retired" state — the operational `status` column is preserved at retirement time as the last-seen value, but `retired_at` is the source of truth for filtering agents out of active listings. retired_reason: type: string nullable: true description: | I-004: human-readable reason captured at retirement time. Only set when the agent was retired via `?force=true&reason=...` cascade; a default soft-retire leaves this field null. AgentDependencyCounts: type: object description: | I-004: preflight counts of active downstream rows that would be orphaned by retiring an agent. Returned in the 409 `blocked_by_dependencies` body so the operator UI can tell the user which bucket is blocking the retire, and also in the 200 response body on a successful `?force=true` cascade as a snapshot of what was cascaded. properties: active_targets: type: integer description: Deployment targets with this agent assigned and retired_at IS NULL active_certificates: type: integer description: Certificates currently deployed via one of this agent's active targets pending_jobs: type: integer description: Jobs with agent_id=this in status Pending, AwaitingCSR, AwaitingApproval, or Running RetireAgentResponse: type: object description: | I-004: response body for a successful retire on DELETE /api/v1/agents/{id}. Returned on both clean retires (cascade=false, zero counts) and force-cascade retires (cascade=true, counts snapshot of the pre-cascade dependency state). The 204 idempotent-retire path does NOT emit this body — re-retiring an already-retired agent returns an empty response. properties: retired_at: type: string format: date-time already_retired: type: boolean description: | Always false on the 200 response — the already-retired path returns 204 No Content with no body. Surfaced in the schema only so downstream consumers have a complete field map. cascade: type: boolean description: True when the retire was invoked with ?force=true counts: $ref: "#/components/schemas/AgentDependencyCounts" BlockedByDependenciesResponse: type: object description: | I-004: 409 response body for a retire request blocked by active downstream dependencies. Returned when `force=true` is not set and any of the three counts is non-zero. The operator UI renders these counts so the human can retire or reassign the blocking rows before re-running the retire, or tick the force checkbox to cascade. properties: error: type: string example: blocked_by_dependencies message: type: string counts: $ref: "#/components/schemas/AgentDependencyCounts" WorkItem: type: object properties: id: type: string type: $ref: "#/components/schemas/JobType" certificate_id: type: string common_name: type: string sans: type: array items: type: string target_id: type: string target_type: type: string target_config: type: object status: $ref: "#/components/schemas/JobStatus" # ─── Jobs ──────────────────────────────────────────────────────── JobType: type: string enum: [Issuance, Renewal, Deployment, Validation] JobStatus: type: string enum: - Pending - AwaitingCSR - AwaitingApproval - Running - Completed - Failed - Cancelled Job: type: object properties: id: type: string type: $ref: "#/components/schemas/JobType" certificate_id: type: string target_id: type: string status: $ref: "#/components/schemas/JobStatus" attempts: type: integer max_attempts: type: integer last_error: type: string scheduled_at: type: string format: date-time started_at: type: string format: date-time completed_at: type: string format: date-time created_at: type: string format: date-time # ─── Policies ──────────────────────────────────────────────────── PolicyType: type: string enum: - AllowedIssuers - AllowedDomains - RequiredMetadata - AllowedEnvironments - RenewalLeadTime - CertificateLifetime PolicySeverity: type: string enum: [Warning, Error, Critical] PolicyRule: type: object properties: id: type: string name: type: string type: $ref: "#/components/schemas/PolicyType" config: type: object description: Policy-specific configuration (varies by type) enabled: type: boolean severity: $ref: "#/components/schemas/PolicySeverity" description: Severity level applied to violations of this rule. Defaults to Warning on create when omitted. created_at: type: string format: date-time updated_at: type: string format: date-time PolicyViolation: type: object properties: id: type: string certificate_id: type: string rule_id: type: string message: type: string severity: $ref: "#/components/schemas/PolicySeverity" created_at: type: string format: date-time # ─── Renewal Policies ───────────────────────────────────────────── # G-1: renewal_policies table — lifecycle policies, referenced by # managed_certificates.renewal_policy_id ON DELETE RESTRICT. Distinct # from PolicyRule above (compliance rules, table policy_rules). RenewalPolicy: type: object required: - id - name - renewal_window_days - auto_renew - max_retries - retry_interval_seconds - alert_thresholds_days - created_at - updated_at properties: id: type: string description: Human-readable ID, prefixed `rp-` (e.g., `rp-default`). name: type: string description: Unique display name (UNIQUE in DB). renewal_window_days: type: integer minimum: 1 maximum: 365 description: Days before expiry to trigger renewal. auto_renew: type: boolean description: Whether renewal is triggered automatically by the scheduler. max_retries: type: integer minimum: 0 maximum: 10 description: Maximum renewal retry attempts on failure. retry_interval_seconds: type: integer minimum: 60 maximum: 86400 description: Seconds to wait between retry attempts. alert_thresholds_days: type: array items: type: integer minimum: 0 maximum: 365 description: Days-before-expiry thresholds at which to emit alerts. certificate_profile_id: type: string nullable: true description: Optional certificate profile binding. Read-only at this endpoint; UI does not currently edit this field. created_at: type: string format: date-time updated_at: type: string format: date-time RenewalPolicyCreateRequest: type: object required: - name properties: id: type: string description: Optional human-readable ID. Auto-generated from name when omitted. name: type: string minLength: 1 maxLength: 255 renewal_window_days: type: integer minimum: 1 maximum: 365 default: 30 auto_renew: type: boolean default: true max_retries: type: integer minimum: 0 maximum: 10 description: Required. Not defaulted — 0 is a valid operator choice. retry_interval_seconds: type: integer minimum: 60 maximum: 86400 default: 3600 alert_thresholds_days: type: array items: type: integer minimum: 0 maximum: 365 default: [30, 14, 7, 0] RenewalPolicyUpdateRequest: type: object description: Partial update. Omitted fields are left unchanged. properties: name: type: string minLength: 1 maxLength: 255 renewal_window_days: type: integer minimum: 1 maximum: 365 auto_renew: type: boolean max_retries: type: integer minimum: 0 maximum: 10 retry_interval_seconds: type: integer minimum: 60 maximum: 86400 alert_thresholds_days: type: array items: type: integer minimum: 0 maximum: 365 # ─── 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 enum: [pending, sent, failed, dead, read] description: | Notification lifecycle status. I-005 adds `dead` for notifications that exhausted their 5-attempt retry budget and were moved to the dead-letter queue; operators triage these in the GUI's Dead letter tab and use POST /notifications/{id}/requeue to resurrect them. error: type: string retry_count: type: integer description: | Number of delivery attempts made. I-005 retry-sweep field; caps at max_attempts=5 before the notification transitions to `dead`. next_retry_at: type: string format: date-time description: | When the next retry attempt is scheduled. I-005 retry-sweep field; null for `sent`, `dead`, and `read` statuses. Backoff follows `min(2^retry_count * 1m, 1h)`. last_error: type: string description: | Most recent transient delivery error (SMTP failure, webhook 5xx, etc.). I-005 retry-sweep field; surfaced on the Dead letter tab so operators can triage without chasing server logs. 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