Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 96 additions & 23 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,42 +604,115 @@ curl http://localhost:5000/api/client-certs/invalid-id \

## Audit Logging

All API requests are logged for audit purposes.
Certificate-lifecycle operations and configuration/access-control changes are
recorded to an audit log. This includes the security-relevant lifecycle paths —
successful and failed create, renew, reissue, deploy, and auto-renew toggles,
plus **unattended (scheduler-driven) renewals** — each attributed to the actor
that performed it and the trigger that caused it.

### Logged Information
### Log format

- Timestamp
- Operation (create, revoke, download, etc.)
- Resource ID
- User/IP address
- Status (success/failure)
- Response time
- Error details (if any)
The audit log is written to `logs/audit/certificate_audit.log`. Each line is a
standard Python log line whose message is the JSON audit entry:

### Accessing Audit Logs
```
2026-06-15 18:00:00 - certmate.audit - INFO - {"timestamp": "...", ...}
```

To recover the JSON, split each line on the literal ` - INFO - ` and parse the
remainder. Note two time bases: the line prefix timestamp is **local** server
time, while the JSON `timestamp` field is **UTC** (ISO-8601). Read it live with:

```bash
tail -f logs/audit/certificate_audit.log
```

Each entry is JSON formatted for easy parsing:
### Entry shape

```json
{
"timestamp": "2024-10-30T18:00:00Z",
"operation": "create",
"resource_type": "certificate",
"resource_id": "cert-001",
"status": "success",
"user": "admin@example.com",
"ip_address": "192.168.1.1",
"details": {
"common_name": "user@example.com",
"usage": "api-mtls"
},
"error": null
"timestamp": "2026-06-15T18:00:00.000000+00:00",
"operation": "renew",
"resource_type": "certificate",
"resource_id": "api.example.com",
"status": "success",
"user": "api_key:renew-bot",
"ip_address": "10.0.0.9",
"details": {"force": false},
"error": null,
"actor": {
"kind": "agent",
"id": "9f2c…",
"label": "api_key:renew-bot",
"token_prefix": "cm_1a2b",
"agent_session": "sess-9f2"
},
"trigger": {"cause": "agent"}
}
```

- **`actor.kind`** — `user` (a human session / OIDC login), `api_token` (an API
key or the legacy global bearer token), `agent` (an API key explicitly flagged
as an AI/MCP agent — see below), `scheduler` (an unattended renewal job), or
`system`. It is derived **only from the authenticated identity**.
- **`actor.id` / `token_prefix`** — the stable API key id and token prefix behind
the action (absent for the legacy global bearer token, which cannot be told
apart per-caller — prefer scoped keys).
- **`actor.agent_session` / `agent_id`** — the values of the client-supplied
`X-CertMate-Agent-Session` / `X-CertMate-Agent-Id` headers (the MCP server
sends them). These are an **informational claim only**: they are recorded for
correlation but never change `actor.kind`, so a non-agent caller cannot forge
an `agent` attribution.
- **`trigger.cause`** — `manual`, `api`, `agent`, `scheduled_renewal`, or
`event`; for scheduled renewals `trigger.job_id` names the job.

To have an agent's actions recorded as `actor.kind="agent"`, create a scoped API
key with `is_agent: true` (a checkbox on Settings → API Keys, or `is_agent` in
`POST /api/keys`) and point the MCP server at it. See the [MCP guide](./mcp.md).

### Reading the audit log over the API

`GET /api/activity?limit=N` returns the most recent entries (admin/viewer,
bounded to 500).

### Tamper-evidence (hash chain)

Alongside the human-readable log, every entry is appended to a tamper-evident
SHA-256 **hash chain** at `data/audit/certificate_audit.chain.jsonl`. Each record
is `{seq, entry, prev_hash, hash}` where `hash` commits to the entry and the
previous record's hash, and `seq` is a gap-free counter — so any modification,
deletion, or reorder by anyone who cannot recompute the whole chain is
detectable and localizable. It is on by default; disable with
`CERTMATE_AUDIT_CHAIN=0`.

**Verify from the API:** `GET /api/audit/verify` (admin) returns the verifier
result and HTTP `200` when intact or `409` when broken:

```json
{"ok": true, "count": 128, "first_seq": 0, "last_seq": 127, "head_hash": "5ee1…", "reason": "intact"}
```

**Verify off-box:** the standalone verifier depends only on the Python standard
library, so an auditor can run it without installing or trusting CertMate:

```bash
python -m modules.core.audit_verify data/audit/certificate_audit.chain.jsonl
# OK: audit chain intact (128 entries, seq 0..127)
# or: FAIL: audit chain broken at seq 42: hash mismatch at seq 42: entry was modified
```

Exit code `0` intact, `1` broken (with the offending `seq` and reason), `2`
missing/unreadable.

> **Threat-model honesty.** The chain detects an interior modification,
> deletion, or reorder by anyone who does not hold the writer's running state.
> It does **not** detect entries removed from the *end* (tail truncation)
> without an external head anchor, and it does **not** bind the operator, who
> holds the file and could recompute and rewrite the whole chain. Constraining
> the operator (and detecting tail truncation) requires external anchoring of
> signed checkpoints, which this version does not implement. See
> [compliance.md](./compliance.md).

---

## Certificate Types
Expand Down
96 changes: 96 additions & 0 deletions docs/compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Compliance and the audit trail

This page maps CertMate's audit trail to the regimes operators most often ask
about — the EU AI Act, NIS2, and ISO/IEC 42001 — when they let an AI/MCP agent
operate certificates on a schedule.

> **Read this first.** CertMate is a single-instance, self-hosted MIT tool. It is
> **not** an AI system, **not** a high-risk AI system, and **not** a regulated
> entity, and it does not "comply with" or "certify" anything. Compliance
> obligations fall on the **operator** running it. What CertMate provides is
> **evidence artifacts** an operator can use toward *their own* obligations.
> Every claim below is "enables the operator to evidence X", with the limits
> stated explicitly.

## What the audit trail provides today

- **Attribution.** Every certificate-lifecycle action — create, renew, reissue,
deploy, auto-renew toggle, and unattended scheduled renewals — is recorded
with a structured `actor` (human vs API token vs AI agent, down to the API key
id) and `trigger` (manual, API, agent, or the scheduler job). An AI agent's
actions are distinguishable from a human's, provided the agent uses an
`is_agent`-flagged key. See [API: Audit Logging](./api.md#audit-logging) and
the [MCP guide](./mcp.md#audit-attribution).
- **Tamper-evidence.** Entries are written into an append-only SHA-256 hash
chain (`data/audit/certificate_audit.chain.jsonl`). Any modification,
deletion, or reorder by someone who cannot recompute the chain is detectable
and localizable.
- **Independent verification.** A standalone, standard-library-only verifier
(`python -m modules.core.audit_verify`) recomputes the chain and returns
PASS/FAIL without needing to run or trust CertMate; `GET /api/audit/verify`
exposes the same check over the API.

## Regime mapping

### NIS2 (Directive (EU) 2022/2555) — the strongest fit

- **What it helps with.** Certificate operations change the trust posture of
services, so they are security-relevant events. CertMate produces a
tamper-evident, attributed, time-stamped record of every such operation, plus
an independently verifiable check — usable as part of the operator's logging
(Art. 21) and incident-evidence (Art. 23) practices.
- **Limit.** NIS2 binds essential/important **entities**, not software tools.
CertMate supplies logs and a verifier the operator can use; it does not assess,
monitor, or report incidents, and being an in-scope entity (and meeting NIS2 in
full) is the operator's responsibility.

### EU AI Act — Article 50 transparency (spirit only; the weakest fit)

- **What it helps with.** When an AI agent autonomously operates PKI, the record
carries an explicit `actor.kind="agent"` marker plus the agent session, so the
operator can demonstrate after the fact which changes were made by an AI agent
versus a human, under which identity, and what triggered them — supporting the
transparency and human-oversight spirit of the Act.
- **Limit.** Art. 50 duties fall on **providers/deployers of AI systems** and
concern disclosure to natural persons interacting with AI. An agent renewing
TLS certificates is not a textbook Art. 50 case, and CertMate is a tool, not an
AI system. We map to the transparency spirit only; CertMate does **not** satisfy
Art. 50 on anyone's behalf.

### ISO/IEC 42001 (AI management system) — operational records

- **What it helps with.** The attributed, tamper-evident records are objective
evidence that an AI agent took specific certificate actions — usable for the
operational-records and traceability controls of the operator's own AIMS.
- **Limit.** ISO 42001 certifies an organisation's management system, not a tool.
CertMate is not certified to ISO 42001 and cannot certify the operator; it
produces records the operator can present as evidence for their own controls.

## Honest limits (do not over-read these)

- **The chain does not bind the operator.** It detects tampering by anyone
**without** the writer's running state, but the operator holds the file and
could recompute and re-sign the whole chain. Constraining the operator requires
external anchoring of signed checkpoints off the box — **not implemented in
this version**. Treat the current guarantee as "authenticity and ordering of
the recorded entries", verifiable by a third party who holds an exported copy.
- **Authenticity, not completeness.** Audit writes are best-effort and never
block a certificate operation; the chain proves the recorded entries are
authentic and ordered, and a missing interior `seq` proves a deletion, but a
write that failed before it was recorded leaves no entry to verify.
- **Tail truncation is not detected on its own.** Removing entries from the
**end** of the chain leaves a shorter-but-internally-consistent chain that
still verifies as intact — the verifier has no external reference for the
expected head. Detecting tail truncation requires an external head anchor
(the signed-checkpoint anchoring of Phase 3, not in this version). Until then,
treat "intact" as "the entries present are authentic and in order", and keep
an out-of-band record of the latest `head_hash` / `last_seq` if you need to
detect end-removal.
- **The agent-session header is a claim.** It is recorded for correlation but is
client-supplied; the trustworthy identity is the authenticated API key.
- **History boundary.** The chain starts when the feature is first enabled;
earlier `.log` history is not part of the verifiable chain.

If your obligations require binding the operator themselves (off-box anchoring,
signed exports an external auditor pins to a published key), that is planned but
not yet available — track it before relying on it.
28 changes: 27 additions & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ npm install
### Environment Variables
The MCP server communicates with the CertMate REST API and requires two environment variables:
- `CERTMATE_URL` — The URL of your CertMate instance (default: `http://localhost:8000`).
- `CERTMATE_TOKEN` — A valid API Bearer token with appropriate role permissions (typically `operator` or `admin`).
- `CERTMATE_TOKEN` — A valid API Bearer token with appropriate role permissions (typically `operator` or `admin`). For an auditable agent, use a key flagged as an agent key (see [Audit attribution](#audit-attribution)).

Optional:
- `CERTMATE_AGENT_SESSION` — Overrides the per-process session id the server sends on every call (`X-CertMate-Agent-Session`), so a run can be correlated with an external orchestrator's id. A fresh UUID is generated per process if unset.
- `CERTMATE_AGENT_ID` — A label for this agent deployment (`X-CertMate-Agent-Id`, default `certmate-mcp-server`).

### Integration Example (Claude Desktop Config)
To add the CertMate MCP server to Claude Desktop, add the following to your configuration file (usually located at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
Expand Down Expand Up @@ -109,3 +113,25 @@ only if it must change settings or read diagnostics.
1. **Token Protection** — The MCP server requires a valid `CERTMATE_TOKEN`. It passes this token securely in the `Authorization` header for all requests to the CertMate API.
2. **Least privilege** — Scope the token to what the agent needs. A scheduled renew-keeper needs `operator`; reserve `admin` tokens for agents that must change settings or pull diagnostics. Revoke the token to instantly cut the agent off.
3. **Log Sanitization Compatibility** — Tools like `certmate_diagnostics` retrieve data after the Log Sanitizer has stripped sensitive credentials, protecting keys and tokens from leaking into LLM contexts.

## Audit attribution

So the audit trail can tell an agent's actions apart from a human operator's,
give the MCP server a **dedicated, agent-flagged API key** rather than the legacy
global bearer token:

1. In CertMate, go to **Settings → API Keys**, create a key, and tick **AI agent
key** (or send `"is_agent": true` to `POST /api/keys`). Scope it with
`allowed_domains` and the least role it needs.
2. Set that key as `CERTMATE_TOKEN` for the MCP server.

Every certificate action the agent then takes is recorded with
`actor.kind="agent"`, the key's stable id, and the per-process
`X-CertMate-Agent-Session` the server sends — so you can later show exactly which
certificate changes an AI agent made, under which identity, and grouped by run.
The legacy global bearer token collapses every caller to `api_user` with no key
id and is recorded as `api_token`, not `agent`. The agent-session header is an
informational claim and never by itself promotes a caller to `agent`.

The resulting records are part of the tamper-evident audit chain; see
[Audit Logging](./api.md#audit-logging) and [compliance.md](./compliance.md).
13 changes: 12 additions & 1 deletion mcp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ const {
ListToolsRequestSchema,
} = require("@modelcontextprotocol/sdk/types.js");
const fetch = require("node-fetch");
const { randomUUID } = require("crypto");

// Stable id for this agent process, sent on every CertMate call so the audit
// trail can group an agent session's actions. It is an INFORMATIONAL claim:
// CertMate derives the trustworthy actor identity from the authenticated API
// key, never from this header. Override via CERTMATE_AGENT_SESSION (e.g. to
// correlate with an external orchestrator's run id).
const AGENT_SESSION = process.env.CERTMATE_AGENT_SESSION || randomUUID();
const AGENT_ID = process.env.CERTMATE_AGENT_ID || "certmate-mcp-server";

const server = new Server({
name: "certmate-mcp-server",
Expand Down Expand Up @@ -162,7 +171,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {

const headers = {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
"Content-Type": "application/json",
"X-CertMate-Agent-Session": AGENT_SESSION,
"X-CertMate-Agent-Id": AGENT_ID
};

async function makeRequest(method, path, body = null) {
Expand Down
Loading
Loading