# Security Considerations for Remote Unity MCP Server: Authentication, Isolation, and Fail-Closed Design

> Secure your remote Unity MCP server with API keys, user isolation, and fail-closed design. Learn essential security practices to prevent unauthorized access and data leaks.

- Repository: [Coplay/unity-mcp](https://github.com/CoplayDev/unity-mcp)
- Tags: best-practices
- Published: 2026-07-06

---

**Running a remote Unity MCP server requires strict API-key authentication, per-user session isolation, and fail-closed defaults to prevent unauthorized code execution and cross-user data leakage.**

The CoplayDev/unity-mcp project enables AI assistants to control Unity Editor instances through a Model Context Protocol (MCP) server. When operating in **remote-hosted mode**, the Python bridge accepts network connections from external clients, making robust security architecture essential to protect against remote code execution and session hijacking.

## Threat Model and Attack Surface

Remote hosting exposes the Unity Editor to network-based threats that are absent in local-only deployments. The threat model prioritizes preventing arbitrary command execution and enforcing strict boundary controls between users.

### Remote Code Execution Risks

The most critical vulnerability is **unauthorized Unity Editor control via crafted MCP messages**. According to the source code in [`Server/src/transport/unity_instance_middleware.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/unity_instance_middleware.py), every tool call passes through `UnityInstanceMiddleware` which validates the presence of a `user_id` before forwarding commands. Without this injection step, the middleware raises a `RuntimeError` and returns an HTTP 401 response, blocking the request entirely.

### Session Leakage and Cross-User Contamination

In multi-tenant scenarios, one user must never access another's Unity instance. The `PluginRegistry` class in [`Server/src/transport/plugin_registry.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/plugin_registry.py) maintains a dual-index mapping system: local mode uses `_hash_to_session`, while remote-hosted mode uses `_user_hash_to_session` keyed by the tuple `(user_id, project_hash)`. Any attempt to list sessions without providing an explicit `user_id` raises a `ValueError`, preventing enumeration attacks.

## Authentication Architecture

The authentication flow implements defense-in-depth with external validation, in-memory caching, and transport-layer verification.

### API Key Validation Flow

When a client sends an MCP request to the HTTP endpoint `/mcp`, the server expects the header `X-API-Key: <key>`. The `UnityInstanceMiddleware.on_call_tool` method extracts this header and delegates validation to `ApiKeyService.validate()` in [`Server/src/services/api_key_service.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/services/api_key_service.py).

The validation process follows this strict sequence:

1. **Cache Check**: The service checks an internal dict `_cache` mapping `api_key` to `(valid, user_id, metadata, expires_at)`.
2. **External Validation**: If the cache misses or the entry expired, the service performs an HTTP POST to the configurable `validation_url` with the key in the request body.
3. **Result Handling**: Definitive responses (HTTP 200/401) are cached for the duration specified by `--api-key-cache-ttl` (default **300 seconds**). Transient failures (5xx, timeouts) are **never cached**, ensuring temporary outages do not permanently lock out users.
4. **Context Injection**: Valid keys result in `ctx.set_state("user_id", result.user_id)`, making the identity available to downstream tools.

**Credential Safety**: API keys are redacted to `xxxx...yyyy` format before any logging occurs, and plaintext keys are never persisted to disk.

### WebSocket Security for Plugin Connections

Unity plugins connect via WebSocket to `/hub/plugin` as implemented in [`Server/src/transport/plugin_hub.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/plugin_hub.py). The `PluginHub.on_connect` method enforces the same `X-API-Key` header requirement. Invalid authentication triggers specific close codes:

- **4401**: Missing API key
- **4403**: Invalid API key  
- **1013**: Authentication service unavailable (retry later)

This prevents unauthenticated WebSocket sessions from reaching the Unity Editor.

## Session Isolation Mechanisms

Once authenticated, the server must ensure each user interacts only with their assigned Unity instance.

### User-Scoped Session Registry

The `PluginRegistry` class differentiates between local and remote modes through storage structure. In remote-hosted mode, sessions are stored in `_user_hash_to_session` with a composite key of `(user_id, project_hash)`. This design prevents UUID collisions and enforces logical separation at the storage layer.

### Unity Instance Middleware Injection

The `UnityInstanceMiddleware` in [`Server/src/transport/unity_instance_middleware.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/unity_instance_middleware.py) calls `get_session_key(ctx)` to resolve the active instance. The resolution order prefers `client_id`, falls back to `user:{user_id}`, and finally `"global"`. However, when `--http-remote-hosted` is enabled, the global fallback is unreachable, ensuring every operation is tied to an authenticated identity.

## Caching Strategy and Resilience

The `ApiKeyService` implements a **fail-soft** cache for availability without compromising security. Only definitive validation results (explicit `valid=True` or `valid=False` with 401 responses) enter the cache. Network timeouts, connection errors, and 5xx responses return `valid=False` with `cacheable=False`, forcing immediate retry on the next request.

Operators can tune the cache duration via the CLI argument `--api-key-cache-ttl`. Lower values (e.g., 60 seconds) provide faster revocation response at the cost of increased load on the external authentication service.

## Fail-Closed Security Defaults

The architecture follows a **fail-closed** philosophy where any error or missing credential results in denial rather than accidental exposure:

| Component | Failure Mode | Security Response |
|-----------|--------------|-------------------|
| `ApiKeyService._validate_external` | Timeout or connection error | Returns non-cacheable invalid result |
| `PluginHub.on_connect` | Auth service unavailable | Closes WebSocket with code 1013 |
| `UnityInstanceMiddleware._inject_unity_instance` | Missing `user_id` in remote mode | Raises `RuntimeError` → HTTP 401 |
| `list_sessions()` | Called without `user_id` | Raises `ValueError` |

## Configuration and Deployment Hardening

Proper deployment requires explicit opt-in flags and external validation endpoints.

### Required Startup Configuration

Remote-hosted mode activation requires both the feature flag and the validation endpoint. The server aborts startup with exit code 1 if `--api-key-validation-url` is missing while `--http-remote-hosted` is set.

```bash
python -m Server.main \
    --http-remote-hosted \
    --api-key-validation-url https://auth.internal.company.com/validate \
    --api-key-cache-ttl 300 \
    --allow-lan-bind

```

**Security-critical flags**:
- `--allow-lan-bind`: Binds HTTP to `0.0.0.0` instead of loopback (disabled by default)
- `--allow-insecure-remote-http`: Permits plain `http://` URLs (disabled by default; HTTPS enforced)
- `--api-key-service-token-header`: Authenticates the server itself to the external validation service

### Integration Example

Unity clients must include the header during WebSocket initialization:

```csharp
var ws = new WebSocket("wss://mcp.company.com/hub/plugin");
ws.SetRequestHeader("X-API-Key", userApiKey);
ws.Connect();

```

On the server side, tool implementations access the validated instance through the context:

```python
@McpTool(...)
async def update_scene(ctx: Context, command: str):
    # Injected by UnityInstanceMiddleware after auth validation

    unity_instance = ctx.get_state("unity_instance")
    await unity_instance.send_command(command)

```

## Summary

- **Remote Unity MCP server security** relies on mandatory API-key validation via the `X-API-Key` header, enforced by `ApiKeyService` and `UnityInstanceMiddleware`.
- **Session isolation** is achieved through `PluginRegistry`'s user-scoped index `_user_hash_to_session`, preventing cross-user access to Unity instances.
- **Fail-closed defaults** ensure that authentication failures, missing credentials, or service outages result in immediate denial rather than accidental access.
- **Caching strategy** stores only definitive validation results for 300 seconds by default, while transient errors trigger immediate retry without poisoning the cache.
- **Credential protection** redacts API keys from logs and never stores them in plaintext.

## Frequently Asked Questions

### How does the remote Unity MCP server prevent unauthorized code execution?

The server prevents unauthorized code execution through `UnityInstanceMiddleware` in [`Server/src/transport/unity_instance_middleware.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/unity_instance_middleware.py), which validates every incoming request for a `user_id` before forwarding it to the Unity Editor. Unauthenticated requests raise a `RuntimeError` and return an HTTP 401 response, ensuring that only validated sessions can trigger editor commands.

### What happens if the external authentication service goes down?

If the external validation URL becomes unreachable, `ApiKeyService._validate_external` returns a non-cacheable invalid result, causing immediate retry on subsequent requests. For WebSocket connections, `PluginHub.on_connect` closes the connection with code 1013 (service unavailable), signaling clients to retry later without caching the failure state.

### Can one user access another user's Unity project in remote-hosted mode?

No. The `PluginRegistry` in [`Server/src/transport/plugin_registry.py`](https://github.com/CoplayDev/unity-mcp/blob/main/Server/src/transport/plugin_registry.py) uses a user-scoped index `_user_hash_to_session` that keys sessions by `(user_id, project_hash)`. Additionally, the `list_sessions` method requires an explicit `user_id` parameter in remote mode; omitting it raises a `ValueError`, preventing session enumeration or cross-tenant access.

### How are API keys protected in server logs?

All API keys are redacted to a masked format (`xxxx...yyyy`) before any logging occurs in `ApiKeyService`. The keys are validated against an external endpoint and cached in memory only as hashed or masked identifiers, ensuring plaintext credentials never appear in log files or persistent storage.