Gatekit Security Model

Version: 0.1.0
Status: Authoritative Reference

Note: This document describes the ACTUAL behavior of the Gatekit security model as implemented. It serves as the single source of truth for security processing decisions.

Table of Contents

  1. Threat Model
  2. Core Concepts
  3. Plugin Types
  4. Processing Pipeline Flow
  5. Decision Trees
  6. Content Clearing Rules
  7. Critical vs Non-Critical Plugin Handling
  8. Reason Handling
  9. Example Scenarios

Threat Model

This section describes what Gatekit is designed to protect against, what it provides infrastructure for, and what falls outside its scope.

What Gatekit Protects Against

What Gatekit Provides Infrastructure For

Note on built-in security plugins: The built-in plugins (basic_pii_filter, basic_secrets_filter, basic_prompt_injection_defense) use simple regex matching. They catch obvious patterns but not sophisticated obfuscation or encoding. For production use with sensitive data, implement plugins tailored to your specific data formats and threat model.

What Gatekit Does NOT Protect Against

Trust Boundaries

Component Trust Level Notes
MCP Clients Untrusted Gatekit filters what they can access and what data flows to/from them
Upstream Server Responses Filtered Security plugins can inspect and block malicious response content
Upstream Server Processes Trusted Not sandboxed; can access filesystem, network, etc.
Host Filesystem Trusted Config and logs assumed protected by OS permissions

Audit Log Security

Core Concepts

Outcome Enums

StageOutcome

Represents the outcome of a single plugin's processing:

class StageOutcome(Enum):
    ALLOWED = "allowed"                             # Plugin allowed request to continue
    BLOCKED = "blocked"                             # Security plugin blocked the request
    MODIFIED = "modified"                           # Plugin modified the content
    COMPLETED_BY_MIDDLEWARE = "completed_by_middleware"  # Middleware provided a complete response
    ERROR = "error"                                 # Plugin threw an exception

PipelineOutcome

Represents the overall outcome of the entire processing pipeline:

class PipelineOutcome(Enum):
    ALLOWED = "allowed"                            # Request/response allowed through unchanged
    BLOCKED = "blocked"                            # Security plugin blocked
    MODIFIED = "modified"                          # Content was modified but allowed through
    COMPLETED_BY_MIDDLEWARE = "completed_by_middleware"  # Middleware completed the request
    ERROR = "error"                                # Critical plugin error occurred
    NO_SECURITY_EVALUATION = "no_security"         # No security plugins evaluated

Key Data Structures

ProcessingPipeline

Contains the complete processing history:

PipelineStage

Records a single plugin's processing:

Plugin Types

SecurityPlugin

MiddlewarePlugin

Processing Pipeline Flow

Request Processing Flow

  1. Initialize Pipeline

    • Start with PipelineOutcome.NO_SECURITY_EVALUATION
    • Set capture_content = True
    • Track had_critical_error = False locally
  2. For Each Plugin (in priority order):

    a. Execute Plugin

    try:
        result = await plugin.process_request(request, server_name)
    except Exception as e:
        # Handle based on criticality (see Critical vs Non-Critical section)
    

    b. Determine StageOutcome

    • If exception thrown → StageOutcome.ERROR
    • If result.allowed = FalseStageOutcome.BLOCKED
    • If result.completed_responseStageOutcome.COMPLETED_BY_MIDDLEWARE
    • If result.modified_contentStageOutcome.MODIFIED
    • Otherwise → StageOutcome.ALLOWED

    c. Update Pipeline State

    • If SecurityPlugin and result.allowed = True AND pipeline_outcome == NO_SECURITY_EVALUATION: → Set pipeline_outcome = ALLOWED immediately (but processing continues)
    • Track if critical error occurred → Set had_critical_error = True (outcome set later during finalization)
    • When add_stage() is called:
      • If StageOutcome.BLOCKED → Sets pipeline_outcome = BLOCKED and blocked_at_stage = plugin_name immediately
      • If StageOutcome.COMPLETED_BY_MIDDLEWARE → Sets pipeline_outcome = COMPLETED_BY_MIDDLEWARE and completed_by = plugin_name immediately
      • If StageOutcome.ERROR or StageOutcome.MODIFIED → No immediate pipeline outcome change (handled during finalization)

    d. Determine Whether to Continue

    • If StageOutcome.BLOCKED → Stop processing
    • If StageOutcome.COMPLETED_BY_MIDDLEWARE → Stop processing
    • If StageOutcome.ERROR and plugin is critical → Stop processing
    • If StageOutcome.ERROR and plugin is non-critical → Continue processing
    • Otherwise → Continue to next plugin
  3. Finalize Pipeline

    • If had_critical_error → Set pipeline_outcome = ERROR
    • Else if outcome not already final (BLOCKED, COMPLETED_BY_MIDDLEWARE, ERROR):
      • If any stage has StageOutcome.MODIFIED → Set pipeline_outcome = MODIFIED
      • Else if had_security_plugin → Set pipeline_outcome = ALLOWED
      • Otherwise leave as NO_SECURITY_EVALUATION
    • Apply content clearing if needed (see Content Clearing Rules)

Response and Notification Processing

Follow the same flow as Request Processing with appropriate method substitutions.

Decision Trees

1. Should Processing Continue After Plugin?

Plugin Outcome
├── BLOCKED → STOP
├── COMPLETED_BY_MIDDLEWARE → STOP
├── ERROR
│   ├── Plugin is critical → STOP
│   └── Plugin is non-critical → CONTINUE
├── MODIFIED → CONTINUE
└── ALLOWED → CONTINUE

2. Final Pipeline Outcome Determination

Had Critical Error?
├── YES → pipeline_outcome = ERROR
└── NO
    └── Outcome already final (BLOCKED/COMPLETED_BY_MIDDLEWARE/ERROR)?
        ├── YES → Keep current outcome
        └── NO
            └── Any stage modified content?
                ├── YES → pipeline_outcome = MODIFIED
                └── NO
                    └── Had Security Plugin?
                        ├── YES → pipeline_outcome = ALLOWED
                        └── NO → Keep as NO_SECURITY_EVALUATION

3. Audit Logging with Pipeline Outcomes

Audit plugins receive the raw pipeline_outcome and had_security_plugin values, allowing each formatter to interpret the results according to its specific requirements:

Each audit formatter can then decide how to represent these outcomes based on its use case (compliance, debugging, alerting, etc.).

Middleware Outcomes

Middleware plugins operate before security plugins and can optimize or filter tool access for operational purposes (not security). They produce specific outcomes:

COMPLETED_BY_MIDDLEWARE

Occurs when middleware provides a complete response, typically when:

// Request: tools/call for hidden tool
// Middleware returns completed response with error
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "error": {
    "code": -32601,
    "message": "Tool 'dangerous_tool' is not available"
  }
}
// Pipeline outcome: COMPLETED_BY_MIDDLEWARE
// Processing stops, security plugins never see the request

MODIFIED (Middleware-only)

When only middleware acts and modifies content:

// Original tools/list response has 10 tools
// Middleware filters to 3 allowed tools
// Pipeline outcome: MODIFIED (if no security plugins)
// Or further evaluated by security plugins

NO_SECURITY_EVALUATION

This outcome currently serves dual purposes:

  1. No security plugins were configured or ran
  2. No middleware made changes (pass-through)

When middleware plugins are present but don't modify/complete:

Note: There is no separate NO_MIDDLEWARE_EVALUATION outcome. The NO_SECURITY_EVALUATION outcome indicates no security evaluation occurred, regardless of whether middleware was present.

Audit Record Examples for Middleware

When middleware completes a request (e.g., hiding a tool), audit plugins capture the full context:

JSON Lines Format

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "event_type": "REQUEST",
  "direction": "request",
  "server_name": "filesystem",
  "method": "tools/call",
  "id": "req-123",
  "params": {
    "name": "delete_all",
    "arguments": {}
  },
  "pipeline_outcome": "completed_by_middleware",
  "completed_by": "ToolManagerPlugin",
  "had_security_plugin": false,
  "pipeline": {
    "outcome": "completed_by_middleware",
    "total_time_ms": 0.45,
    "stages": [
      {
        "plugin": "ToolManagerPlugin",
        "plugin_type": "middleware",
        "outcome": "completed_by_middleware",
        "time_ms": 0.35,
        "reason": "Tool not in allowlist"
      }
    ]
  },
  "status": "blocked",
  "message": "Tool 'delete_all' is not available"
}

Human-Readable Line Format

2024-01-15 10:30:45 | REQUEST | filesystem | tools/call | req-123 | COMPLETED_BY_MIDDLEWARE | ToolManagerPlugin | Tool 'delete_all' not available (hidden by middleware)

Key differences from security blocks:

Content Clearing Rules

When Content Gets Cleared

Content (input_content, output_content) and reasons are cleared from pipeline stages when:

  1. During Processing: capture_content is set to False when:

    • Any SecurityPlugin returns allowed = False (blocked)
    • Any SecurityPlugin returns modified_content (redacted/modified)
  2. After Processing: If capture_content = False:

    • All stage input_contentNone
    • All stage output_contentNone
    • All stage reasons → Generic like "[allowed]", "[blocked]", "[error]"
    • Content hashes are KEPT for lineage tracking

Security Rationale

This prevents sensitive data (that triggered blocks or was redacted) from appearing in logs.

Critical vs Non-Critical Plugin Handling

Plugin Criticality Defaults

Exception Handling Based on Criticality

When a plugin throws an exception:

except Exception as e:
    outcome = StageOutcome.ERROR
    if plugin.is_critical():
        had_critical_error = True
        # Log as error
        # Stop processing after this stage
    else:
        # Log as warning  
        # Continue processing to next plugin

Important: Criticality ONLY affects exception handling. It does NOT affect security decisions (allowed = False always stops processing regardless of criticality).

Reason Handling

Individual Plugin Reasons

Each plugin can provide a custom reason in its PluginResult. These are preserved in each PipelineStage.result.reason.

Pipeline-Level Reason (for audit logging)

As of v0.1.0, all plugin reasons are concatenated with | separator in the BaseAuditingPlugin._combine_pipeline_reasons() method, with each reason prefixed by the plugin name in brackets:

# In BaseAuditingPlugin class
def _combine_pipeline_reasons(self, pipeline: ProcessingPipeline, modified_stage: Optional['PipelineStage']) -> str:
    """Combine reasons from all pipeline stages into a single string for logging."""
    # Collect all non-empty reasons from pipeline stages in execution order
    reasons = []
    for stage in pipeline.stages:
        if stage.result and stage.result.reason:
            # Include plugin name with each reason for better traceability
            reason_with_plugin = f"[{stage.plugin_name}] {stage.result.reason}"
            reasons.append(reason_with_plugin)
    
    # If we have plugin reasons, concatenate them with pipe separator
    if reasons:
        return " | ".join(reasons)
    
    # Fallback to generic pipeline outcome if no plugin reasons available
    return pipeline.pipeline_outcome.value

Example: "[Tool Manager] Tool 'read_file' is in allowlist | [Basic PII Filter] No PII detected | [Basic Secrets Filter] No secrets detected"

Reason Clearing

When capture_content = False, reasons are replaced with generic values (still prefixed with plugin name):


Edge Cases and Special Behaviors

Edge Case 1: Plugin Contract Violations

The system enforces strict contracts for each plugin type:

MiddlewarePlugin Setting allowed (Contract Violation):

SecurityPlugin Not Setting allowed (Contract Violation):

Design Principle: Only SecurityPlugin instances can make security decisions. MiddlewarePlugin instances that need to block requests must be implemented as SecurityPlugin subclasses.

Edge Case 2: Multiple Security Plugin Outcomes

When multiple security plugins evaluate a message, the pipeline outcome follows these rules:

  1. First allowed=False wins: Processing stops immediately at the first security plugin that blocks
  2. All must allow: For the message to proceed, ALL security plugins must return allowed=True
  3. Modification doesn't stop processing: If a security plugin modifies content but returns allowed=True, processing continues with the modified content
  4. Pipeline outcome priority: BLOCKED > ALLOWED > NO_SECURITY_EVALUATION

Edge Case 3: Pipeline Outcome Timing (Immediate vs Deferred)

Pipeline outcomes are set at different times based on their impact on processing flow:

Immediate Outcomes (set as soon as they occur):

  1. BLOCKED - Set by add_stage() when any security plugin blocks (stops processing immediately)
  2. COMPLETED_BY_MIDDLEWARE - Set by add_stage() when middleware completes response (stops processing immediately)
  3. ALLOWED - Set when first security plugin allows AND outcome is still NO_SECURITY_EVALUATION (processing continues but outcome is locked in)

Deferred Outcomes (set during finalization): 4. ERROR - Set during finalization if had_critical_error flag is true

  1. MODIFIED - Set during finalization if any stage has StageOutcome.MODIFIED
    • Why deferred: Lower priority than BLOCKED/COMPLETED/ERROR, so we wait to see if a higher-priority outcome occurs
  2. NO_SECURITY_EVALUATION - Remains if no security plugins ran and no other outcome was set

The finalization logic checks outcomes in priority order: ERROR > BLOCKED/COMPLETED (already set) > MODIFIED > ALLOWED (already set) > NO_SECURITY_EVALUATION

Important: Once set to BLOCKED or COMPLETED_BY_MIDDLEWARE, the outcome doesn't change (processing stops). MODIFIED is set during finalization and takes precedence over ALLOWED.

Edge Case 4: Content Clearing Triggers

Content gets cleared (capture_content = False) when:

  1. Security block: Any SecurityPlugin returns allowed=False
  2. Security modification: Any SecurityPlugin returns modified_content
  3. NOT for middleware modifications: Middleware modifications don't trigger content clearing

This is a security feature - any security action (block or redact) triggers clearing to prevent sensitive data from appearing in logs.


NO_SECURITY_EVALUATION Behavior

When the final pipeline outcome is NO_SECURITY_EVALUATION:

This is intentional - the proxy does not block by default when no security plugins are configured.

Example Scenarios

Scenario 1: Single Security Plugin - Allowed

Setup: One security plugin (Tool Manager) evaluates a request

# Plugin execution
ToolManagerPlugin.process_request()  PluginResult(allowed=True, reason="Tool 'read_file' is in allowlist")

Pipeline Result:

Scenario 2: Single Security Plugin - Blocked

Setup: One security plugin blocks a request

# Plugin execution
ToolManagerPlugin.process_request()  PluginResult(allowed=False, reason="Tool 'dangerous_tool' not in allowlist")

Pipeline Result:

Scenario 3: Multiple Security Plugins - Mixed Decisions

Setup: Three security plugins evaluate in sequence

# Plugin execution order
1. ToolManagerPlugin.process_request()  PluginResult(allowed=True, reason="Tool 'read_file' is in allowlist")
2. BasicPIIFilterPlugin.process_request()  PluginResult(allowed=True, modified_content=..., reason="PII detected and redacted: email")
3. BasicSecretsFilterPlugin.process_request()  PluginResult(allowed=True, reason="No secrets detected")

Pipeline Result:

Scenario 4: Critical Plugin Error

Setup: A critical security plugin throws an exception

# Plugin execution
CriticalSecurityPlugin.process_request()  throws Exception("Database connection failed")
# Plugin has critical=True (default for SecurityPlugin)

Pipeline Result:

Scenario 5: Non-Critical Plugin Error Continues

Setup: A non-critical plugin fails, then a critical plugin succeeds

# Plugin execution order
1. NonCriticalMonitoringPlugin.process_request()  throws Exception("Metrics service unavailable")
   # Plugin has critical=False
2. CriticalSecurityPlugin.process_request()  PluginResult(allowed=True, reason="Request authorized")

Pipeline Result:

Scenario 6: Middleware Completion

Setup: Middleware provides a complete response

# Plugin execution order
1. SecurityPlugin.process_request()  PluginResult(allowed=True, reason="Allowed")
2. CacheMiddleware.process_request()  PluginResult(
       allowed=None,
       completed_response=MCPResponse(...),
       reason="Served from cache"
   )

Pipeline Result:

Scenario 7: No Security Plugins

Setup: Only middleware plugins configured

# Plugin execution order
1. LoggingMiddleware.process_request()  PluginResult(allowed=None, reason="Request logged")
2. MetricsMiddleware.process_request()  PluginResult(allowed=None, reason="Metrics recorded")

Pipeline Result:

Scenario 8: Security Plugin Modifies Content

Setup: Security plugin redacts sensitive data

# Plugin execution
BasicSecretsFilterPlugin.process_response()  PluginResult(
    allowed=True,
    modified_content=MCPResponse(...),  # with secrets redacted
    reason="3 secrets redacted"
)

Pipeline Result:

Scenario 9: Middleware Sets allowed=False (Contract Violation)

Setup: A middleware plugin incorrectly tries to make a security decision

# Plugin execution
LoggingMiddleware.process_request()  PluginResult(allowed=False, reason="Suspicious activity")
# Note: LoggingMiddleware extends MiddlewarePlugin, not SecurityPlugin
# This raises ValueError during contract enforcement

Pipeline Result:

Key Point: Contract violations are errors. Middleware plugins attempting to make security decisions indicates incorrect plugin type usage.


Summary

Key Takeaways

  1. Security Plugins are Special: Only SecurityPlugin subclasses can make security decisions that block messages
  2. Pipeline Outcomes are Final: BLOCKED and COMPLETED_BY_MIDDLEWARE stop processing immediately
  3. Content Clearing is Security-Driven: Only security actions (block/modify) trigger content clearing
  4. Criticality Affects Errors Only: Critical vs non-critical only matters for exception handling, not for security decisions
  5. NO_SECURITY_EVALUATION is Permissive: Messages pass through when no security plugins are configured
  6. Reasons are Concatenated: All plugin reasons are joined with | for audit logging
  7. Contract Violations are Errors: Both plugin types must follow their contracts or face exceptions

Implementation Notes

This document reflects the actual behavior as of Gatekit v0.1.0. The implementation is in: