This document describes the security model for the RCAC MCP Server, including how command execution is isolated across different deployment modes.
The server supports three execution modes, each with different security characteristics:
| Mode | Transport | Auth | Use Case |
|---|---|---|---|
ssh |
stdio | N/A | Local MCP client → remote HPC cluster |
local |
http/sse | none | Development/testing only |
delegate |
http/sse | jwt/oidc | Multi-user production deployment |
Intended use: Desktop MCP clients (Claude Desktop, Cursor, etc.) connecting to remote HPC clusters.
┌─────────────────┐ stdio ┌─────────────────┐ SSH ┌─────────────┐
│ MCP Client │ ◄──────────────► │ rcac-mcp │ ◄────────────► │ HPC Cluster │
│ (Claude, etc.) │ │ (local) │ │ │
└─────────────────┘ └─────────────────┘ └─────────────┘
Security model:
- The MCP server runs locally on the user's machine
- SSH connection uses the user's existing
~/.ssh/configand keys - No authentication at the MCP layer (stdio is inherently single-user)
- All commands execute as the SSH user on the remote system
- Security boundary is the SSH connection itself
Configuration:
rcac-mcp --ssh-host cluster.example.edu
# or
export RCAC_SSH_HOST=cluster.example.edu
rcac-mcpTrust assumptions:
- The local user is trusted (they control the MCP client)
- The SSH connection is authenticated via standard SSH mechanisms
- The remote system enforces permissions via standard Unix file/process ownership
Intended use: Development and testing only. NOT FOR PRODUCTION.
┌─────────────────┐ HTTP ┌─────────────────┐
│ MCP Client │ ◄──────────────► │ rcac-mcp │ ──► local $SHELL
│ │ │ (http server) │
└─────────────────┘ └─────────────────┘
Security model:
- Commands execute directly on the server as the process owner
- No authentication - anyone who can reach the HTTP endpoint can execute commands
- Single shared executor for all requests
Configuration:
rcac-mcp -t http -e localIntended use: Multi-user production deployment where the server runs as root and delegates commands to authenticated users.
┌─────────────────┐ ┌─────────────────┐
│ User A │ ──► JWT/OIDC ──► │ │ ──► sudo -u alice ...
│ (sub: alice) │ │ │
└─────────────────┘ │ rcac-mcp │
│ (runs as root) │
┌─────────────────┐ │ │
│ User B │ ──► JWT/OIDC ──► │ │ ──► sudo -u bob ...
│ (sub: bob) │ │ │
└─────────────────┘ └─────────────────┘
Security model:
- Server runs as root (or a privileged user with sudo rights)
- Each request is authenticated via JWT or OIDC
- User identity is extracted from token claims (
sub,email, orpreferred_username) - Identity is mapped to a local Unix user via the user map file
- Commands are wrapped with
sudo -n -H -u <local_user> ... - OS-level controls (sudoers, PAM, SELinux, etc.) enforce what each user can do
Configuration:
export JWT_SECRET="your-secret-key-at-least-32-characters"
export RCAC_USER_MAP=/etc/rcac-mcp/users.map
rcac-mcp -t http -a jwt -e delegateUser map file format (/etc/rcac-mcp/users.map):
# Identity claim → local Unix user
# Lines starting with # are comments
alice@purdue.edu alice
bob@purdue.edu bob
service-account-1 svc_batch
A critical security property of delegate mode is that concurrent requests from
different users are properly isolated. This is achieved using Python's
contextvars.ContextVar mechanism.
How it works:
from contextvars import ContextVar
# This is NOT a global variable - it's a context-local variable
executor_var: ContextVar[Executor] = ContextVar('executor', default=None)Context variables are task-local in async Python. Each async task (HTTP request) gets its own isolated copy of the context:
Request 1 (alice) Request 2 (bob)
───────────────────────── ─────────────────────────
on_call_tool middleware on_call_tool middleware
│ │
▼ ▼
set_executor( set_executor(
DelegatingExecutor(alice) DelegatingExecutor(bob)
) )
│ │
▼ ▼
get_executor() → alice's executor get_executor() → bob's executor
│ │
▼ ▼
sudo -u alice <command> sudo -u bob <command>
Why this is safe:
-
No shared state: Each request's executor is stored in its own context, not in a module-level global variable.
-
Automatic isolation: FastMCP/Starlette creates a new async task per request, which automatically gets a fresh context copy.
-
No cross-request leakage: When request 1 calls
set_executor(), it only affects request 1's context. Request 2 sees its own value. -
Clean teardown: When a request completes, its context is discarded.
Contrast with an unsafe approach:
# ❌ WRONG - This would be a security vulnerability!
_current_executor = None # Shared across ALL requests
def set_executor(executor):
global _current_executor
_current_executor = executor # Race condition! User A could get User B's executorThe ContextVar approach is the same pattern used by:
- FastMCP's
get_access_token()for per-request auth context - Starlette's request state
- Python's
decimalmodule for per-context precision settings
For delegate mode to work, the server process must be able to run commands as mapped users without a password prompt. Example sudoers configuration:
# /etc/sudoers.d/rcac-mcp
# Allow rcac-mcp (running as root or rcac-mcp user) to run commands as any user
# NOPASSWD is required since there's no TTY for password prompts
rcac-mcp ALL=(ALL) NOPASSWD: ALL
# Or more restrictively, only allow specific users:
rcac-mcp ALL=(alice,bob,svc_batch) NOPASSWD: ALL
Recommended restrictions:
- Limit which users can be delegated to
- Use
NOEXECto prevent execution of binaries from writable directories - Consider using SELinux/AppArmor for additional confinement
In delegate mode, the middleware extracts user identity from JWT/OIDC token claims in the following order of preference:
sub- Standard JWT subject claim (most common)email- Common in OIDC providerspreferred_username- Used by some OIDC providers (Keycloak, etc.)
The first non-empty claim found is used as the key to look up the local user in the user map file.
Example JWT payload:
{
"sub": "alice@purdue.edu",
"iss": "rcac-mcp",
"aud": "rcac-mcp",
"exp": 1706650000
}This would be mapped using the alice@purdue.edu key from the user map.
- Use
localmode withauth=none - Never expose to network
- Use
sshmode via stdio - Leverage existing SSH key infrastructure
- Use
delegatemode withjwtoroidcauth - Run server as dedicated service account with minimal privileges
- Configure restrictive sudoers rules
- Enable TLS (use reverse proxy like nginx)
- Audit log all commands (future enhancement)
- Regularly rotate JWT secrets / OIDC credentials
If you discover a security vulnerability, please report it privately to rcac-help@purdue.edu rather than opening a public issue.