Topic: mcp server sandboxing security

MCP server sandboxing security — seccomp, AppArmor, container isolation, and process confinement for tool execution

MCP tool handlers execute with the same OS privileges as the server process. If a tool handler contains a command injection vulnerability, a deserialization flaw, or a path traversal that reaches an executable, the attacker inherits the full capability set of the server: filesystem access, network access, environment variables, the ability to spawn child processes, and potentially the ability to read other processes' memory. Sandboxing separates the security boundary between "tool handler exploited" and "host compromised" — a separation that does not exist in unsandboxed MCP server deployments.

The blast radius without sandboxing

A Node.js MCP server running as a normal user process has access to:

An exploit that reaches code execution in a tool handler has unrestricted access to all of the above. Sandboxing replaces the single failure mode ("tool handler exploited = host compromised") with two distinct failure modes: "tool handler exploited = tool's declared capability set accessible" and (separately) "sandbox itself compromised = host accessible." The second failure requires a significantly more sophisticated exploit chain.

Layer 1 — seccomp syscall filtering

seccomp (secure computing mode) is a Linux kernel feature that filters which system calls a process is allowed to make. A node process serving a simple MCP server that reads files and makes HTTP requests does not need syscalls like ptrace, mount, init_module, keyctl, or syslog. Blocking those syscalls with a seccomp policy makes an entire class of privilege escalation attacks impossible — the exploited process simply cannot call the syscall the exploit depends on:

# Docker run with default seccomp profile (blocks ~44 dangerous syscalls)
docker run --security-opt seccomp=/etc/docker/seccomp/default.json mcp-server

# Custom minimal seccomp profile for a stdio MCP server (no network)
# Allow only: read, write, exit, rt_sigreturn, fstat, mmap, mprotect, brk,
#             close, openat, getdents64, clock_gettime, futex, epoll_wait
# Block: ptrace, execve, fork, clone, socket, connect, bind, listen
# (no outbound HTTP, no process spawning, no ptrace)

# Generate from strace baseline + manually deny dangerous calls:
# strace -f -e trace=all node server.js 2>&1 | grep SYSCALL | sort -u > syscalls.txt
# Then build seccomp JSON allowing only those syscalls.

Layer 2 — AppArmor/SELinux MAC profiles

Mandatory access control (MAC) enforces a policy at the kernel level that restricts which files, network operations, and capabilities a process can access, regardless of what the process itself tries to do:

# /etc/apparmor.d/mcp-server-stdio — AppArmor profile for a stdio-only MCP server
#include <tunables/global>

profile mcp-server-stdio /usr/local/bin/node {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Allow reading the server's own files
  /app/server/** r,
  /app/node_modules/** r,

  # Allow writing to the designated log directory only
  /app/logs/*.log w,

  # Allow reading tool-permitted files (example: a specific data directory)
  /app/data/** r,

  # Deny everything else — no write to /etc, no read from /root, no network
  deny /etc/** w,
  deny /root/** rwx,
  deny network,

  # Allow process-necessary capabilities only
  capability setuid,    # if the server drops privileges after startup
  deny capability sys_ptrace,
  deny capability sys_admin,
}

Layer 3 — containerization with minimal attack surface

Container isolation adds network namespace separation (the container cannot reach services on the host network unless explicitly mapped), a separate PID namespace (cannot see or signal host processes), and filesystem isolation (read-only root with only mounted volumes writable):

# Dockerfile hardened for MCP server deployment
FROM node:22-alpine AS base

# Non-root user with minimal home directory
RUN addgroup -S mcpgroup && adduser -S mcpuser -G mcpgroup

FROM base AS runtime
WORKDIR /app
COPY --chown=mcpuser:mcpgroup package*.json ./
RUN npm ci --omit=dev

COPY --chown=mcpuser:mcpgroup src/ ./src/
USER mcpuser

# No EXPOSE — stdio only, no port mapping needed

# docker run options:
# --read-only                      (root filesystem read-only)
# --tmpfs /tmp:size=10m            (allow writes only to tmpfs /tmp)
# --network none                   (no network for stdio-only servers)
# --no-new-privileges              (prevents privilege escalation via setuid)
# --cap-drop ALL                   (drop all Linux capabilities)
# --cap-add ...                    (re-add only the specific ones needed)
# --security-opt no-new-privileges
# --security-opt seccomp=seccomp.json

Layer 4 — tool execution isolation via subprocess sandboxing

For MCP servers that execute external programs (file converters, code interpreters, data processors), each tool execution should run in its own subprocess with an even more restricted sandbox than the server process itself:

import { spawn } from 'child_process';

async function runToolSubprocess(command, args, inputData) {
  return new Promise((resolve, reject) => {
    // execFile(false) — no shell, args are not interpreted by a shell
    const child = spawn(command, args, {
      shell: false,
      stdio: ['pipe', 'pipe', 'pipe'],
      timeout: 5000,                   // Hard timeout — prevents runaway processes
      uid: SANDBOX_UID,               // Dedicated low-privilege UID
      gid: SANDBOX_GID,
      env: {
        // Minimal env — no credentials, no PATH to unexpected directories
        PATH: '/usr/local/bin:/usr/bin',
        TMPDIR: '/tmp/mcp-sandbox',
      },
    });

    child.stdin.write(inputData);
    child.stdin.end();

    let stdout = '';
    let stderr = '';
    child.stdout.on('data', chunk => { stdout += chunk; });
    child.stderr.on('data', chunk => { stderr += chunk; });

    child.on('close', (code) => {
      if (code !== 0) return reject(new Error(`Tool exited ${code}: ${stderr}`));
      resolve(stdout);
    });
  });
}

SkillAudit detection

SkillAudit flags the following sandboxing-related findings in the Security axis:

Sandboxing is the defense-in-depth layer that makes the other SkillAudit findings less severe. A server with a command injection vulnerability in a sandboxed environment has a contained blast radius; the same vulnerability in an unsandboxed server grants full host access. See the minimal-footprint MCP server post for the Principle 4 (no shell invocation) that removes the code path that sandboxing is designed to contain.