diff --git a/api/openapi.yaml b/api/openapi.yaml index a8186ac..a86cecc 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -58,6 +58,10 @@ tags: 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 paths: # ─── Health & Auth ─────────────────────────────────────────────────── @@ -1900,7 +1904,7 @@ paths: get: tags: [Metrics] summary: System metrics - description: JSON metrics snapshot with gauges, counters, and uptime. Prometheus format deferred to V3. + description: JSON metrics snapshot with gauges, counters, and uptime. See also /api/v1/metrics/prometheus for Prometheus exposition format. operationId: getMetrics responses: "200": @@ -1912,6 +1916,384 @@ paths: "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" + # ═══════════════════════════════════════════════════════════════════════ components: securitySchemes: @@ -2578,3 +2960,221 @@ components: 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 diff --git a/docs/demo-advanced.md b/docs/demo-advanced.md index 9129eb7..d7cbbaf 100644 --- a/docs/demo-advanced.md +++ b/docs/demo-advanced.md @@ -372,6 +372,39 @@ curl -s "$API/api/v1/jobs" | jq '.data[] | select(.certificate_id == "mc-demo-ap --- +## Part 4.5: Manage Deployment Targets + +Before deploying, you need targets. The demo seeds 5 targets, but you can also create, update, and delete them via API: + +```bash +# List all targets +curl -s "$API/api/v1/targets" | jq '.data[] | {id, name, type, agent_id}' + +# Create a new NGINX target +curl -s -X POST "$API/api/v1/targets" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "tgt-nginx-api", + "name": "API NGINX", + "type": "nginx", + "agent_id": "ag-web-prod", + "config": {"cert_path": "/etc/nginx/certs/api.crt", "key_path": "/etc/nginx/certs/api.key", "reload_command": "systemctl reload nginx"}, + "enabled": true + }' | jq . + +# Update a target +curl -s -X PUT "$API/api/v1/targets/tgt-nginx-api" \ + -H "Content-Type: application/json" \ + -d '{"name": "API NGINX (updated)", "type": "nginx", "agent_id": "ag-web-prod", "config": {"cert_path": "/etc/nginx/certs/api.crt"}, "enabled": true}' | jq . + +# Delete a target +curl -s -X DELETE "$API/api/v1/targets/tgt-nginx-api" +``` + +Each target type (NGINX, Apache, HAProxy, F5, IIS) accepts different configuration fields. The `config` JSON is validated at deployment time by the target connector. + +--- + ## Part 5: Deploy the Certificate Trigger deployment to see the deployment workflow: diff --git a/docs/features.md b/docs/features.md index 761ac40..2a01ab8 100644 --- a/docs/features.md +++ b/docs/features.md @@ -128,6 +128,21 @@ curl -H "$AUTH" "$SERVER/api/v1/certificates?expires_before=2026-04-24T00:00:00Z - Server signs and stores certificate version - Work endpoint enriched with `common_name` and `sans` for agent CSR generation +### Deployment Trigger +Push certificates to targets on demand, outside of the normal scheduler-driven flow: + +```bash +# Deploy to all mapped targets +curl -X POST -H "$AUTH" $SERVER/api/v1/certificates/mc-api-prod/deploy + +# Deploy to a specific target +curl -X POST -H "$AUTH" -H "$CT" $SERVER/api/v1/certificates/mc-api-prod/deploy \ + -d '{"target_id": "tgt-nginx-prod"}' + +# Check deployment job status +curl -H "$AUTH" "$SERVER/api/v1/certificates/mc-api-prod/deployments" | jq '.data[] | {id, name, type}' +``` + --- ## Revocation Infrastructure @@ -192,6 +207,17 @@ curl -X POST -H "$AUTH" -H "$CT" $SERVER/api/v1/profiles -d '{ curl -X PUT -H "$AUTH" -H "$CT" $SERVER/api/v1/certificates/mc-api-prod -d '{ "profile_id": "prof-short-lived" }' + +# List all profiles +curl -H "$AUTH" "$SERVER/api/v1/profiles" | jq '.data[] | {id, name, max_ttl_hours, allowed_key_algorithms}' + +# Get profile details +curl -H "$AUTH" "$SERVER/api/v1/profiles/prof-standard-tls" | jq . + +# Update profile constraints +curl -X PUT -H "$AUTH" -H "$CT" $SERVER/api/v1/profiles/prof-standard-tls -d '{ + "name": "Standard TLS", "max_ttl_hours": 2160, "allowed_key_algorithms": ["RSA", "ECDSA"] +}' ``` | Field | Details | @@ -519,6 +545,15 @@ Each discovered certificate is parsed and its metadata extracted: | `/api/v1/discovery-scans` | GET | List scan history with timestamps | | `/api/v1/discovery-summary` | GET | Aggregate status counts (Unmanaged, Managed, Dismissed) | +```bash +# Check triage status at a glance +curl -H "$AUTH" "$SERVER/api/v1/discovery-summary" | jq . +# → {"Unmanaged": 12, "Managed": 45, "Dismissed": 3} + +# Review scan execution history +curl -H "$AUTH" "$SERVER/api/v1/discovery-scans" | jq '.data[] | {agent_id, certificates_found, certificates_new, started_at}' +``` + ### Use Cases - **Inventory Baseline** — Scan production servers at deployment time to establish baseline of existing certificates - **Compliance Discovery** — Find all TLS certs before renewing certificate policies @@ -889,7 +924,7 @@ The web dashboard is the primary operational interface for certctl. Built with * ### OpenAPI 3.1 Specification - **File** — `api/openapi.yaml` -- **Scope** — 78 documented operations (spec covers core API; discovery and network scan endpoints pending addition) +- **Scope** — 93 operations (91 API + /health + /ready), all request/response schemas, enums, pagination - **Schemas** — Complete domain models with examples - **Enums** — Job types, states, policy rule types, notification types - **Pagination** — Standard envelope (data, total, page, per_page)