> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmcp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Immutable Audit Log

> Every call log row is HMAC-signed at write time so any tampering is detectable. Use the built-in verifier to check your audit trail.

## Overview

GetMCP signs every call log entry with an **HMAC-SHA256 signature** at the moment it is written. If any stored field is later modified — whether accidentally or deliberately — the signature no longer matches, and the verifier flags the row as tampered.

This makes the call log effectively **append-only**: entries can be deleted (and that deletion is itself detectable by gaps), but they cannot be silently edited.

***

## How Signing Works

When a call log row is inserted into `wp_getmcp_call_logs`, GetMCP:

1. Assembles a **canonical string** from the row's key fields in a fixed order.
2. Derives a **signing key** from WordPress's `AUTH_KEY` constant using HKDF with the domain string `getmcp-log-signing-v1`.
3. Computes `HMAC-SHA256(canonical_string, signing_key)`.
4. Stores the 64-character hex digest in the `signature` column of the same row.

The signing key is zeroed from memory after use (`sodium_memzero`) when the PHP sodium extension is available.

***

## Canonical Fields

The canonical string includes the following fields (joined with `|`):

| Field              | Description                                |
| ------------------ | ------------------------------------------ |
| `id`               | Auto-increment row ID                      |
| `server_id`        | Server that received the call              |
| `tool_id`          | Tool that was called                       |
| `method`           | MCP method (`tools/call`)                  |
| `arguments`        | Sanitized input arguments (JSON)           |
| `response_status`  | `success` or `error`                       |
| `response_time_ms` | Round-trip time in milliseconds            |
| `status_code`      | Upstream HTTP status code                  |
| `client_type`      | Detected AI client                         |
| `client_ip`        | Caller IP address                          |
| `user_agent`       | Caller user-agent string                   |
| `error_message`    | Error text (if applicable)                 |
| `response_data`    | Full response (if Log Response Data is on) |
| `created_at`       | UTC timestamp                              |

Fields **not** included in the canonical form (so adding them later doesn't break old signatures):

* `upstream_bytes` / `delivered_bytes`
* `replay_data`

***

## Verifying the Audit Log

<img src="https://mintlify.s3.us-west-1.amazonaws.com/infiwebs/images/audit-log-verify-button.png" alt="Logs page — More actions kebab menu with Verify integrity item" />

Go to **GetMCP → Logs**, open the **More actions** (`⋮`) kebab menu in the toolbar, and choose **Verify integrity**. GetMCP will:

1. Fetch all rows matching your current filter (date range, server, status).
2. Recompute the HMAC for each row.
3. Compare it to the stored `signature`.
4. Return a summary report.

### Verification result states

<img src="https://mintlify.s3.us-west-1.amazonaws.com/infiwebs/images/audit-log-verification-results.png" alt="Audit log verification summary showing Verified, Tampered, and Unsigned row counts" />

| State                             | Meaning                                                                     |
| --------------------------------- | --------------------------------------------------------------------------- |
| **Verified**                      | Signature matches — row is intact                                           |
| **Tampered**                      | Signature does not match — field values were modified after write           |
| **Unsigned (legacy)**             | Row has no signature — written before audit log was introduced (DB \< 1.12) |
| **Unsigned (signer unavailable)** | `AUTH_KEY` is missing or too short; signing was skipped at write time       |

<Note>
  Rows marked **Unsigned (legacy)** are not evidence of tampering — they simply pre-date the feature. Only rows with a stored signature can be verified.
</Note>

***

## Requirements

| Requirement           | Detail                                                                                                     |
| --------------------- | ---------------------------------------------------------------------------------------------------------- |
| **AUTH\_KEY**         | Must be defined in `wp-config.php` and at least 16 characters — standard on all healthy WordPress installs |
| **`hash_hmac`**       | PHP core since 5.1.2 — always available                                                                    |
| **`sodium_memzero`**  | Optional — used to zero the key from memory after signing; signing still works without it                  |
| **DB version ≥ 1.12** | The `signature` column is added by the 1.12 migration                                                      |

***

## Security Considerations

### Key rotation

The signing key is derived from `AUTH_KEY`. If you rotate `AUTH_KEY` (e.g. after a security incident), all existing signatures will fail verification — they were signed with the old key. This is expected behaviour. After rotation, treat all pre-rotation rows as **unsigned legacy** rows.

### What tampering looks like

A tampered row has a stored `signature` value that no longer matches the HMAC of its current field values. The verifier flags it as **Tampered** and shows the row ID so you can investigate.

Common accidental causes of failed verification:

* Directly editing rows in phpMyAdmin or a MySQL client
* A database migration that modified existing rows
* Restoring a partial DB backup that mixed rows from two different `AUTH_KEY` periods

### What the audit log does not prevent

The audit log **detects** tampering — it does not prevent it. A user with direct MySQL access can delete rows or the entire table. Use database-level access controls and regular off-site backups for defence-in-depth.

***

## Daily Automated Verification

A WordPress cron job (`getmcp_daily_audit_verify`) runs once per day and re-verifies every signed row from the previous calendar day (UTC). If any row's signature does not match, GetMCP sends an alert via `wp_mail()` to the notification email configured at **GetMCP → Settings → Notifications** (falls back to the WordPress `admin_email` if unset). No email is sent on a clean run.

The same scan can be triggered ad-hoc against any filter range from **GetMCP → Logs → More actions → Verify integrity**, or via `POST /wp-json/getmcp/v1/analytics/audit/verify` for scripted full-history sweeps.

A `getmcp_audit_tampering_detected` action also fires with the list of tampered row IDs, so you can hook into it from a custom plugin (e.g. to forward to a SIEM).
