mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 16:08:51 +00:00
feat(mcp): 11 audit-fix MCP tools — approvals, break-glass, bootstrap, audit-category (MED-13)
Audit 2026-05-10 MED-13 closure.
WHAT.
11 new MCP tools rounding out the operator surface for workflows
that previously had GUI + CLI coverage but no MCP equivalent:
Approval workflow (4):
certctl_approval_list GET /v1/approvals approval.read
certctl_approval_get GET /v1/approvals/{id} approval.read
certctl_approval_approve POST /v1/approvals/{id}/approve approval.approve
certctl_approval_reject POST /v1/approvals/{id}/reject approval.reject
Break-glass credential admin (4):
certctl_breakglass_list GET /v1/auth/breakglass/credentials
certctl_breakglass_set_password POST /v1/auth/breakglass/credentials
certctl_breakglass_unlock POST /v1/auth/breakglass/credentials/{actor_id}/unlock
certctl_breakglass_remove DELETE /v1/auth/breakglass/credentials/{actor_id}
All gated auth.breakglass.admin; surface invisible (404 not 403)
when CERTCTL_BREAKGLASS_ENABLED=false.
Bootstrap (2):
certctl_bootstrap_status GET /v1/auth/bootstrap (auth-exempt; safe probe)
certctl_bootstrap_consume POST /v1/auth/bootstrap (auth-exempt; one-shot mint)
Audit category filter (1):
certctl_audit_list_with_category GET /v1/audit?category=<cat> audit.read
WHY.
certctl_bootstrap_consume is the load-bearing day-0 primitive: a
fresh server with no admin actors lets the holder of CERTCTL_BOOTSTRAP_TOKEN
mint a fresh admin API key. Exposing it via MCP without a security
gate would let a downstream caller mint admin from any chat
transcript / log surface that captured the bootstrap token. The
tool description carries an explicit cautious-wording comment:
CAUTION: NEVER WIRE THIS TO AUTONOMOUS OPERATION. A leaked
bootstrap token from any log, telemetry, or chat-transcript
surface lets a downstream caller mint a fresh admin API key
bypassing every other access-control gate. Run this manually,
exactly once, from a trusted shell.
Similarly certctl_breakglass_set_password's description flags
that the password crosses the MCP transport in plaintext; the
server-side handler hashes with Argon2id before persisting + the
audit row redacts, but client-side logging must NEVER capture the
payload.
HOW.
internal/mcp/tools_audit_fix.go (NEW):
registerAuditFixTools(s, c) — declares the 11 tools via
gomcp.AddTool. Each tool routes through the existing Client.Get/
Post/Delete helpers; the server-side rbacGate wrappers (or
auth-exempt allowlist, for bootstrap) handle authorization.
internal/mcp/types.go:
Adds 5 input structs:
ApprovalIDInput (get/approve/reject)
BreakglassActorIDInput (unlock/remove)
BreakglassSetPasswordInput (set_password — flagged plaintext)
BootstrapConsumeInput (token + key_name; cautious comment)
AuditListWithCategoryInput (category + optional limit/since/until/actor_id)
Each tagged with jsonschema descriptions for LLM tool discovery.
internal/mcp/tools.go:
RegisterTools now calls registerAuditFixTools after the existing
Bundle 2 Phase 9 registrar.
internal/mcp/tools_per_tool_test.go:
allHappyPathCases extended with 11 new entries. The existing
TestMCP_AllTools_HappyPath dispatches each tool via the in-memory
MCP transport against a 2xx mock backend and asserts the
wrapper-layer fence wraps the response; TestMCP_AllTools_ErrorPath
dispatches against a 5xx mock and asserts MCP_ERROR fence.
TestMCP_RegisterTools_DispatchableToolCount confirms every new
tool is dispatchable by name.
VERIFY.
- go vet ./internal/mcp/... PASS
- go test -short -count=1
-run 'TestMCP_AllTools_HappyPath|TestMCP_AllTools_ErrorPath|
TestMCP_RegisterTools_DispatchableToolCount'
./internal/mcp/... PASS
- go test -short -count=1 ./internal/mcp/... PASS (0.3s)
Refs: cowork/auth-bundles-audit-2026-05-10.md MED-13
cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 4
This commit is contained in:
@@ -689,3 +689,49 @@ type AuthListSessionsInput struct {
|
||||
type AuthRevokeSessionInput struct {
|
||||
ID string `json:"id" jsonschema:"Session ID (e.g. ses-abc123). Server-side own-bypass: caller may revoke their own session even without auth.session.revoke."`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Audit 2026-05-10 MED-13 — input shapes for the 11 new MCP tools
|
||||
// (approvals + breakglass + bootstrap + audit category filter).
|
||||
// =============================================================================
|
||||
|
||||
// ApprovalIDInput is the id-only input for approval get/approve/reject.
|
||||
type ApprovalIDInput struct {
|
||||
ID string `json:"id" jsonschema:"Approval request ID (e.g. aprq-abc123). Returned by certctl_approval_list."`
|
||||
}
|
||||
|
||||
// BreakglassActorIDInput is the actor-id-only input for the unlock + remove tools.
|
||||
type BreakglassActorIDInput struct {
|
||||
ActorID string `json:"actor_id" jsonschema:"Break-glass actor ID (e.g. bg-admin1). Listed by certctl_breakglass_list."`
|
||||
}
|
||||
|
||||
// BreakglassSetPasswordInput is the body for certctl_breakglass_set_password.
|
||||
//
|
||||
// SECURITY: the password field crosses the MCP transport in
|
||||
// plaintext. The server-side handler hashes with Argon2id before
|
||||
// persisting; the audit row redacts the password column. Never log
|
||||
// this payload at the client side.
|
||||
type BreakglassSetPasswordInput struct {
|
||||
ActorID string `json:"actor_id" jsonschema:"Break-glass actor ID (e.g. bg-admin1). New row created if not present."`
|
||||
Password string `json:"password" jsonschema:"Plaintext password (hashed server-side with Argon2id). Choose >=14 chars from a strong-entropy source; this is the SSO-bypass credential."`
|
||||
RoleID string `json:"role_id" jsonschema:"Role ID granted on successful break-glass login (e.g. r-admin). Typically r-admin for production break-glass."`
|
||||
}
|
||||
|
||||
// BootstrapConsumeInput is the body for certctl_bootstrap_consume.
|
||||
//
|
||||
// SECURITY: NEVER wire this tool into autonomous operation. A leaked
|
||||
// bootstrap token mints a fresh admin API key bypassing every other
|
||||
// access-control gate. Run manually, once, from a trusted shell.
|
||||
type BootstrapConsumeInput struct {
|
||||
Token string `json:"token" jsonschema:"The pre-shared CERTCTL_BOOTSTRAP_TOKEN value (one-shot, constant-time-compared server-side, never logged)."`
|
||||
KeyName string `json:"key_name" jsonschema:"Human-readable name for the new admin API key (e.g. 'day-zero-admin'). Subsequently visible in certctl_auth_list_keys."`
|
||||
}
|
||||
|
||||
// AuditListWithCategoryInput is the input for the category-filtered audit list.
|
||||
type AuditListWithCategoryInput struct {
|
||||
Category string `json:"category,omitempty" jsonschema:"Audit category filter. One of: auth, pki, config, system, security. Empty returns unfiltered (equivalent to GET /v1/audit)."`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"Maximum rows to return. Server default applies when 0."`
|
||||
Since string `json:"since,omitempty" jsonschema:"RFC3339 timestamp lower bound (inclusive). Optional."`
|
||||
Until string `json:"until,omitempty" jsonschema:"RFC3339 timestamp upper bound (exclusive). Optional."`
|
||||
ActorID string `json:"actor_id,omitempty" jsonschema:"Filter by originating actor ID. Optional."`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user