|
| 1 | +# Shared Key Authentication for Local MCP Servers |
| 2 | + |
| 3 | +## Problem Statement |
| 4 | + |
| 5 | +Local ToolHive deployments bind MCP servers to localhost (`127.0.0.1`) for network-level security, but have no authentication layer. While localhost binding prevents remote access, customers want defense-in-depth protection against: |
| 6 | + |
| 7 | +- Malicious local processes connecting to open localhost ports |
| 8 | +- Port forwarding or SSH tunneling accidentally exposing endpoints |
| 9 | +- Privilege escalation attacks targeting unauthenticated local services |
| 10 | +- Compliance requirements mandating authentication even for localhost |
| 11 | + |
| 12 | +Current authentication options (OIDC, local user auth) require external identity providers or manual password management, making them unsuitable for single-user local deployments. |
| 13 | + |
| 14 | +## Goals |
| 15 | + |
| 16 | +- Provide lightweight authentication for localhost MCP server deployments |
| 17 | +- Require zero or minimal configuration from users |
| 18 | +- Leverage existing ToolHive infrastructure (OS keychain, middleware, stdio bridge) |
| 19 | +- Work transparently with the `thv proxy stdio` bridge used by MCP clients |
| 20 | +- Maintain backward compatibility (opt-in feature) |
| 21 | + |
| 22 | +## Non-Goals |
| 23 | + |
| 24 | +- Replace network isolation or container-based security |
| 25 | +- Support multi-user local deployments (use OIDC instead) |
| 26 | +- Protect against root/administrator access or OS keychain compromise |
| 27 | +- Store credentials in configuration files |
| 28 | + |
| 29 | +## Proposed Solution |
| 30 | + |
| 31 | +Implement a shared key authentication middleware that generates unique cryptographic keys per MCP server workload, stores them in the OS keychain, and validates them on incoming requests. |
| 32 | + |
| 33 | +### Architecture |
| 34 | + |
| 35 | +``` |
| 36 | +┌─────────────────────────────────────────────────────────────┐ |
| 37 | +│ MCP Client │ |
| 38 | +│ (Claude Desktop, etc.) │ |
| 39 | +└──────────────────────┬──────────────────────────────────────┘ |
| 40 | + │ stdio |
| 41 | + ↓ |
| 42 | +┌─────────────────────────────────────────────────────────────┐ |
| 43 | +│ thv proxy stdio Bridge │ |
| 44 | +│ • Reads shared key from OS keychain │ |
| 45 | +│ • Injects X-ToolHive-Auth header in HTTP requests │ |
| 46 | +└──────────────────────┬──────────────────────────────────────┘ |
| 47 | + │ HTTP + X-ToolHive-Auth header |
| 48 | + ↓ |
| 49 | +┌─────────────────────────────────────────────────────────────┐ |
| 50 | +│ ToolHive HTTP Proxy │ |
| 51 | +│ • Shared Key Auth Middleware validates header │ |
| 52 | +│ • Constant-time comparison with env variable │ |
| 53 | +│ • Other middleware (Parser, Authz, Audit) │ |
| 54 | +└──────────────────────┬──────────────────────────────────────┘ |
| 55 | + │ Forwarded request |
| 56 | + ↓ |
| 57 | +┌─────────────────────────────────────────────────────────────┐ |
| 58 | +│ MCP Server Container │ |
| 59 | +│ • Receives TOOLHIVE_SHARED_KEY env variable │ |
| 60 | +└─────────────────────────────────────────────────────────────┘ |
| 61 | +``` |
| 62 | + |
| 63 | +### Key Components |
| 64 | + |
| 65 | +**1. Key Generation and Storage** (`pkg/auth/sharedkey/`) |
| 66 | +- Generate cryptographically-secure 32-byte keys using `crypto/rand` |
| 67 | +- Store in OS keychain via existing encrypted secrets provider |
| 68 | +- Key naming: `workload:<workload-name>:shared-key` |
| 69 | +- Lifecycle: Generate on deployment, delete on workload removal |
| 70 | + |
| 71 | +**2. Shared Key Authentication Middleware** (`pkg/auth/sharedkey/middleware.go`) |
| 72 | +- Implements standard `types.Middleware` interface |
| 73 | +- Validates `X-ToolHive-Auth` header against `TOOLHIVE_SHARED_KEY` environment variable |
| 74 | +- Uses `crypto/subtle.ConstantTimeCompare` to prevent timing attacks |
| 75 | +- Returns `401 Unauthorized` for missing/invalid keys |
| 76 | +- Adds identity to context for audit trail |
| 77 | + |
| 78 | +**3. Workload Integration** (`pkg/workloads/manager.go`) |
| 79 | +- Generate and store shared key when `SharedKeyAuth.Enabled` is set in RunConfig |
| 80 | +- Inject key into container and proxy via `TOOLHIVE_SHARED_KEY` environment variable |
| 81 | +- Configure middleware chain automatically |
| 82 | +- Clean up key on workload deletion |
| 83 | + |
| 84 | +**4. Stdio Bridge Enhancement** (`cmd/thv/app/proxy_stdio.go`) |
| 85 | +- Retrieve shared key from secrets provider on startup |
| 86 | +- Inject `X-ToolHive-Auth` header in all HTTP requests via custom transport wrapper |
| 87 | +- Handle 401 responses with informative error messages |
| 88 | + |
| 89 | +**5. RunConfig Extension** (`pkg/runner/config.go`) |
| 90 | + |
| 91 | +```go |
| 92 | +type SharedKeyAuthConfig struct { |
| 93 | + Enabled bool `json:"enabled"` |
| 94 | + KeyRotationDays int `json:"keyRotationDays,omitempty"` // 0 = disabled |
| 95 | +} |
| 96 | + |
| 97 | +// Add to RunConfig struct |
| 98 | +SharedKeyAuth *SharedKeyAuthConfig `json:"sharedKeyAuth,omitempty"` |
| 99 | +``` |
| 100 | + |
| 101 | +## High-Level Design |
| 102 | + |
| 103 | +### Request Flow |
| 104 | + |
| 105 | +1. **Workload Deployment**: User runs `thv run my-server --shared-key-auth` |
| 106 | +2. **Key Generation**: Workload manager generates 32-byte random key |
| 107 | +3. **Key Storage**: Key stored as `workload:my-server:shared-key` in OS keychain |
| 108 | +4. **Key Injection**: Key injected via `TOOLHIVE_SHARED_KEY` environment variable to container and proxy |
| 109 | +5. **Middleware Configuration**: Shared key auth middleware added to chain |
| 110 | +6. **Client Configuration**: MCP client configured to use `thv proxy stdio my-server` |
| 111 | + |
| 112 | +### Authentication Flow |
| 113 | + |
| 114 | +1. **Client Request**: MCP client sends JSON-RPC over stdio to `thv proxy stdio` |
| 115 | +2. **Key Retrieval**: Stdio bridge retrieves key from OS keychain |
| 116 | +3. **Header Injection**: Bridge adds `X-ToolHive-Auth: <key>` to HTTP request |
| 117 | +4. **Validation**: Middleware validates header against environment variable |
| 118 | +5. **Forwarding**: On success, request forwarded to MCP server container |
| 119 | +6. **Rejection**: On failure, return `401 Unauthorized` |
| 120 | + |
| 121 | +### Security Properties |
| 122 | + |
| 123 | +- **Key Strength**: 32 bytes (256 bits), base64-encoded |
| 124 | +- **Key Generation**: Crypto-secure random via `crypto/rand` |
| 125 | +- **Storage**: AES-256-GCM encrypted in OS keychain (existing provider) |
| 126 | +- **Comparison**: Constant-time to prevent timing attacks |
| 127 | +- **Uniqueness**: Independent key per workload |
| 128 | +- **Lifecycle**: Keys deleted with workload |
| 129 | + |
| 130 | +## Usage Examples |
| 131 | + |
| 132 | +### Basic Usage |
| 133 | + |
| 134 | +```bash |
| 135 | +# Enable shared key authentication |
| 136 | +thv run my-server --shared-key-auth |
| 137 | + |
| 138 | +# ToolHive automatically: |
| 139 | +# 1. Generates secure key |
| 140 | +# 2. Stores in OS keychain |
| 141 | +# 3. Configures middleware |
| 142 | +# 4. Updates client config |
| 143 | +``` |
| 144 | + |
| 145 | +### With Other Security Features |
| 146 | + |
| 147 | +```bash |
| 148 | +# Combine with authorization and audit |
| 149 | +thv run my-server --shared-key-auth \ |
| 150 | + --authz-config authz.yaml \ |
| 151 | + --audit-config audit.yaml |
| 152 | +``` |
| 153 | + |
| 154 | +### Debugging |
| 155 | + |
| 156 | +```bash |
| 157 | +# Verbose mode |
| 158 | +thv proxy stdio my-server --verbose |
| 159 | + |
| 160 | +# Inspect key |
| 161 | +thv secret get workload:my-server:shared-key |
| 162 | +``` |
| 163 | + |
| 164 | +## Operational Considerations |
| 165 | + |
| 166 | +### Key Lifecycle |
| 167 | + |
| 168 | +**Generation**: Automatic on deployment with `--shared-key-auth` flag |
| 169 | +**Storage**: OS keychain via encrypted secrets provider |
| 170 | +**Deletion**: Automatic on `thv rm my-server` |
| 171 | + |
| 172 | +### Backward Compatibility |
| 173 | + |
| 174 | +- Opt-in feature via flag or RunConfig |
| 175 | +- Existing workloads continue working without changes |
| 176 | +- No breaking changes to configurations |
| 177 | +- Gradual migration: enable per-workload incrementally |
| 178 | + |
| 179 | +### Multi-Workload Scenarios |
| 180 | + |
| 181 | +- Each workload has unique key |
| 182 | +- Keys identified by workload name |
| 183 | +- Stdio bridge automatically selects correct key |
| 184 | +- Group operations work transparently |
| 185 | + |
| 186 | +## Security Considerations |
| 187 | + |
| 188 | +### Threat Model |
| 189 | + |
| 190 | +**Protected Against**: |
| 191 | +- Unauthorized local process connections |
| 192 | +- Port scanning attacks |
| 193 | +- Accidental credential exposure in config files |
| 194 | +- Replay attacks (keys are workload-scoped) |
| 195 | + |
| 196 | +**Not Protected Against**: |
| 197 | +- Root/administrator access to keychain |
| 198 | +- Process memory inspection |
| 199 | +- Physical machine access |
| 200 | +- Compromised OS keychain |
| 201 | + |
| 202 | +### Defense in Depth |
| 203 | + |
| 204 | +Shared key authentication adds a layer to existing security: |
| 205 | + |
| 206 | +1. Network isolation (localhost binding) |
| 207 | +2. Container isolation |
| 208 | +3. **Shared key authentication** ← New layer |
| 209 | +4. Authorization (Cedar policies) |
| 210 | +5. Audit logging |
| 211 | + |
| 212 | +Each layer provides independent protection. |
| 213 | + |
| 214 | +## Alternatives Considered |
| 215 | + |
| 216 | +### Certificate-Based Authentication (mTLS) |
| 217 | + |
| 218 | +**Pros**: Industry-standard, strong crypto |
| 219 | +**Cons**: Complex (certificate generation, validation, expiry), TLS infrastructure overhead, overkill for localhost |
| 220 | +**Decision**: Rejected due to complexity |
| 221 | + |
| 222 | +### Unix Domain Sockets |
| 223 | + |
| 224 | +**Pros**: Filesystem-based permissions, OS-level access control |
| 225 | +**Cons**: Not cross-platform (Windows), breaks HTTP architecture, requires significant changes |
| 226 | +**Decision**: Rejected due to platform limitations |
| 227 | + |
| 228 | +### API Keys in Config Files |
| 229 | + |
| 230 | +**Pros**: Simple implementation, easy debugging |
| 231 | +**Cons**: Plaintext storage, git commit risk, no leverage of existing secrets infrastructure |
| 232 | +**Decision**: Rejected due to poor security properties |
| 233 | + |
| 234 | +### Time-Based One-Time Passwords (TOTP) |
| 235 | + |
| 236 | +**Pros**: Strong authentication, industry-standard |
| 237 | +**Cons**: Time synchronization complexity, poor UX for automated clients, overkill for localhost |
| 238 | +**Decision**: Rejected due to complexity and poor UX |
| 239 | + |
| 240 | +## Implementation Plan |
| 241 | + |
| 242 | +### Phase 1: Core Infrastructure |
| 243 | +- Create `pkg/auth/sharedkey/` package |
| 244 | +- Implement key generation and storage |
| 245 | +- Implement shared key authentication middleware |
| 246 | +- Add middleware factory registration |
| 247 | +- Extend RunConfig schema |
| 248 | + |
| 249 | +### Phase 2: Workload Integration |
| 250 | +- Modify workloads manager for key lifecycle |
| 251 | +- Add CLI flag `--shared-key-auth` |
| 252 | +- Configure middleware chain automatically |
| 253 | +- Implement key cleanup on deletion |
| 254 | + |
| 255 | +### Phase 3: Stdio Bridge Enhancement |
| 256 | +- Enhance `thv proxy stdio` to retrieve keys |
| 257 | +- Implement header injection in HTTP transport |
| 258 | +- Add error handling for auth failures |
| 259 | +- Update client configuration generation |
| 260 | + |
| 261 | +### Phase 4: Testing and Documentation |
| 262 | +- Unit tests for middleware and key management |
| 263 | +- Integration tests for end-to-end flows |
| 264 | +- Security testing (timing attacks, etc.) |
| 265 | +- Update architecture documentation |
| 266 | +- Create user guide |
| 267 | + |
0 commit comments