Published on

AutoGPT에 MCP로 로컬툴 연결 - 권한·샌드박싱

Authors

서론

AutoGPT류의 에이전트는 “생각”만 잘하는 것이 아니라 “행동”까지 수행합니다. 로컬 파일을 읽고, 쉘 명령을 실행하고, DB에 접속하고, 브라우저를 자동화할 수 있죠. 문제는 이 행동이 곧 로컬 시스템 권한을 의미한다는 점입니다. 에이전트가 실수하거나(프롬프트 오염, 잘못된 계획), 악의적으로 유도되면(도구 설명 조작, 데이터 주입) 로컬 환경이 그대로 공격 표면이 됩니다.

MCP(Model Context Protocol)는 모델/에이전트가 외부 도구를 표준 방식으로 호출할 수 있게 해주는 프로토콜로, “로컬툴을 안전하게 연결”하기 위한 좋은 출발점입니다. 하지만 MCP를 쓴다고 자동으로 안전해지진 않습니다. 결국 핵심은 다음 두 가지입니다.

  • 권한: 어떤 도구에 어떤 범위로 접근을 허용할지
  • 샌드박싱: 도구 실행이 시스템 전체로 번지지 않게 어떻게 격리할지

이 글에서는 AutoGPT에 MCP로 로컬툴을 붙일 때의 보안 설계 원칙, 실전 패턴, 그리고 최소한의 코드 예제를 통해 안전장치를 어떻게 구성하는지 정리합니다.

MCP로 로컬툴을 붙일 때 생기는 위협 모델

로컬 MCP 서버(도구 제공자)를 띄우고 에이전트가 이를 호출하게 만들면, 공격 표면이 다음처럼 확장됩니다.

1) 프롬프트 주입으로 인한 위험한 툴 호출

예: 문서/웹페이지/로그에 “이 명령을 실행하라” 같은 텍스트가 섞여 들어오고, 에이전트가 이를 지시로 오해해 rm -rf 류의 명령을 실행하는 케이스입니다.

2) 도구 설명(스키마) 자체가 공격 벡터

MCP 도구는 이름, 설명, 파라미터 스키마를 노출합니다. 설명에 과도한 권한을 암시하거나 “이 도구는 안전하다” 같은 문구가 있으면, 모델이 검증 없이 실행할 확률이 올라갑니다.

3) 데이터 유출

로컬 파일, SSH 키, 브라우저 쿠키, .env 등이 도구를 통해 조회 가능해지면, 의도치 않게 외부로 전송될 수 있습니다. 특히 RAG나 로그 수집과 결합되면 “민감정보가 컨텍스트로 흘러가는” 문제가 생깁니다. RAG를 운영한다면 벡터DB/인덱싱 단계에서 민감정보 필터링과 권한 분리가 매우 중요합니다. 관련해서는 RAG 성능 2배 - Qdrant HNSW 튜닝 실전도 함께 참고하면 좋습니다.

4) 무한 실행/자원 고갈

에이전트가 반복적으로 툴을 호출하면서 CPU, 디스크, 네트워크를 고갈시키는 형태입니다. 이는 보안 이슈이기도 하고 운영 이슈이기도 합니다.

설계 원칙: 최소권한 + 격리 + 감사

원칙 1) 최소권한(Least Privilege)

  • “로컬 쉘 실행” 같은 범용 도구는 최대한 피합니다.
  • 꼭 필요하다면 허용 명령을 화이트리스트로 제한합니다.
  • 파일 접근은 디렉터리 단위로 allowlist를 둡니다.
  • 네트워크 접근은 기본 차단 후 필요한 도메인만 허용합니다.

원칙 2) 격리(Isolation)

  • MCP 서버 자체를 컨테이너/샌드박스에서 실행합니다.
  • 호스트 파일시스템은 읽기 전용 마운트 또는 제한된 디렉터리만 마운트합니다.
  • 민감한 환경변수는 주입하지 않습니다.

원칙 3) 감사(Audit)와 재현성

  • 모든 툴 호출에 대해 입력/출력/실행시간/실패 사유를 구조화 로그로 남깁니다.
  • “누가 어떤 프롬프트로 어떤 도구를 호출했는가”를 추적 가능해야 합니다.
  • 운영환경에서는 도구 호출을 트레이싱 ID로 묶어 관찰성을 확보합니다.

권한 설계: 도구를 작게 쪼개고, 위험도를 등급화

로컬툴을 “하나의 만능 도구”로 제공하면 통제하기 어렵습니다. 대신 도구를 작게 쪼개고 위험도에 따라 정책을 다르게 적용합니다.

  • Read-only 도구: 파일 읽기, 상태 조회, 로그 tail 등
  • Mutating 도구: 파일 쓰기, git commit, 패키지 설치 등
  • Exec 도구: 쉘 실행, 프로세스 실행(가장 위험)

권장 패턴은 다음과 같습니다.

  1. Read-only 도구부터 시작
  2. Mutating 도구는 “작업 디렉터리”를 분리하고 롤백 전략을 둠
  3. Exec 도구는 가능한 금지, 불가피하면 명령 화이트리스트 + 타임아웃 + 리소스 제한

샌드박싱 전략 4가지

1) 컨테이너 격리(Docker)로 MCP 서버를 감싸기

가장 현실적인 접근입니다.

  • 읽기 전용 루트 파일시스템
  • 특정 작업 폴더만 마운트
  • 네트워크 차단 또는 제한
  • CPU/메모리 제한

예시 Docker 실행(개념 예시):

docker run --rm \
  --read-only \
  --pids-limit=256 \
  --cpus=1 \
  --memory=512m \
  -v "$PWD/work":/work:rw \
  -w /work \
  --network=none \
  mcp-local-tools:latest

이렇게 하면 에이전트가 어떤 도구를 호출하더라도 “컨테이너 바깥”으로 나가기 어렵습니다.

2) OS 샌드박스(리눅스라면 seccomp/AppArmor)

컨테이너를 쓰더라도 시스템 콜을 더 줄일 수 있습니다. 예를 들어 파일 삭제, 네트워크 소켓 생성 같은 위험 동작을 정책으로 제한합니다.

3) 파일시스템 가상화(작업 공간 분리)

  • 에이전트 전용 workspace 디렉터리를 만들고 그 외 경로는 접근 불가
  • 결과물만 외부로 export

특히 “로컬 개발환경 홈 디렉터리”를 그대로 마운트하는 것은 피해야 합니다. .ssh, .aws, .npmrc, .gitconfig 같은 민감한 설정이 한 번에 노출됩니다.

4) 네트워크 격리

  • 기본 deny 후 allowlist
  • 로컬호스트 접근도 제한(예: 127.0.0.1 에 떠 있는 DB/Redis에 붙는 순간 데이터 유출 가능)

쿠버네티스 환경이라면 egress 통제가 중요합니다. 클러스터에서 네트워크가 간헐적으로 끊기거나 NAT/SNAT 이슈를 겪는다면 EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법처럼 네트워크 경로를 먼저 안정화한 뒤, 정책 기반 egress 제한으로 넘어가는 게 운영상 안전합니다.

MCP 도구 구현 예제: “안전한 파일 읽기”와 “제한된 명령 실행”

여기서는 Node.js로 MCP 서버를 만든다고 가정하고, 두 가지 도구를 제공합니다.

  • read_file: 허용된 디렉터리에서만 파일 읽기
  • run_safe_cmd: 화이트리스트 명령만 실행, 타임아웃 적용

주의: 아래 코드는 개념 예시이며, 실제 MCP SDK에 따라 서버 부트스트랩 코드는 달라질 수 있습니다. 핵심은 “정책 레이어”를 도구 호출 전에 반드시 통과시키는 구조입니다.

1) 경로 allowlist 기반 파일 읽기

import fs from "node:fs/promises";
import path from "node:path";

const ALLOWED_ROOTS = [
  path.resolve(process.cwd(), "work"),
];

function assertAllowedPath(p: string) {
  const resolved = path.resolve(p);
  const ok = ALLOWED_ROOTS.some((root) => resolved.startsWith(root + path.sep) || resolved === root);
  if (!ok) {
    throw new Error("Access denied: path is outside allowed roots");
  }
  return resolved;
}

export async function readFileTool(input: { filePath: string }) {
  const resolved = assertAllowedPath(input.filePath);
  const stat = await fs.stat(resolved);
  if (!stat.isFile()) throw new Error("Not a file");

  // 용량 제한(예: 1MB)
  if (stat.size > 1024 * 1024) throw new Error("File too large");

  const content = await fs.readFile(resolved, "utf-8");
  return { content };
}

포인트는 다음입니다.

  • path.resolve 후 루트 prefix 검사로 디렉터리 탈출을 막음
  • 파일 타입/크기 제한
  • 반환은 텍스트로 제한(바이너리 덤프 방지)

2) 화이트리스트 명령 실행 + 타임아웃

exec 류의 도구는 가장 위험합니다. 그래도 필요하다면 “명령 전체 문자열”을 받지 말고, “명령 이름 + 인자 배열”로 받고, 허용된 명령만 실행하세요.

import { spawn } from "node:child_process";

const ALLOWED_CMDS: Record<string, { argsAllowlist?: RegExp[] }> = {
  "git": { argsAllowlist: [/^(status|diff|log)$/] },
  "node": { argsAllowlist: [/^--version$/] },
};

function assertAllowedCommand(cmd: string, args: string[]) {
  const policy = ALLOWED_CMDS[cmd];
  if (!policy) throw new Error("Access denied: command not allowed");

  // 매우 보수적으로: 첫 번째 인자만 검사하는 예시
  if (policy.argsAllowlist && args[0]) {
    const ok = policy.argsAllowlist.some((re) => re.test(args[0]));
    if (!ok) throw new Error("Access denied: args not allowed");
  }
}

export async function runSafeCmdTool(input: { cmd: string; args: string[]; timeoutMs?: number }) {
  const timeoutMs = input.timeoutMs ?? 3000;
  assertAllowedCommand(input.cmd, input.args);

  return await new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve, reject) => {
    const child = spawn(input.cmd, input.args, {
      stdio: ["ignore", "pipe", "pipe"],
      env: {},
    });

    let stdout = "";
    let stderr = "";

    const timer = setTimeout(() => {
      child.kill("SIGKILL");
      reject(new Error("Command timeout"));
    }, timeoutMs);

    child.stdout.on("data", (d) => (stdout += d.toString("utf-8")));
    child.stderr.on("data", (d) => (stderr += d.toString("utf-8")));

    child.on("error", (err) => {
      clearTimeout(timer);
      reject(err);
    });

    child.on("close", (code) => {
      clearTimeout(timer);
      resolve({ stdout, stderr, exitCode: code ?? -1 });
    });
  });
}

추가로 권장하는 방어:

  • env: {} 처럼 환경변수 제거(토큰 유출 방지)
  • cwd 를 작업 디렉터리로 고정
  • 출력 크기 제한(예: 64KB)
  • 프로세스 수 제한(컨테이너 pids-limit)

“사람 승인(Human-in-the-loop)” 게이트를 어디에 둘 것인가

권한과 샌드박싱을 해도, 결국 “데이터 변경”이나 “외부 전송” 같은 고위험 동작은 사람 승인이 안전합니다.

실전에서는 다음 패턴이 많이 쓰입니다.

  • Read-only는 자동 실행
  • Mutating는 dry-run 결과를 먼저 보여주고 승인 후 실행
  • Exec/네트워크 전송은 기본 차단, 승인 토큰이 있을 때만 1회 실행

예를 들어 git diff 는 자동, git commit 은 승인 필요 같은 식입니다.

운영 체크리스트: 사고를 막는 디테일

1) 로깅과 추적

  • 도구 호출마다 requestId 를 부여
  • 입력 파라미터는 민감정보 마스킹
  • 결과는 요약만 저장(원문 전체 저장은 유출 리스크)

2) 캐시/아티팩트 관리

에이전트가 생성한 파일이 캐시에 남아 다른 실행에 영향을 주는 경우가 많습니다. CI에서 캐시가 꼬이면 재현이 어려워지듯, 에이전트 워크스페이스도 동일합니다. 캐시가 의도대로 동작하지 않거나 오염될 때의 관점은 GitHub Actions 캐시가 안 먹힐 때 원인 9가지에서 힌트를 얻을 수 있습니다.

3) 타임아웃/재시도 정책

  • 도구 호출 타임아웃을 짧게
  • 재시도는 멱등성 있는 도구에만
  • 실패 시 원인을 컨텍스트로 되돌리되, 민감정보는 제거

4) 동시성 제어

에이전트가 여러 작업을 병렬로 돌리면 파일 락, DB 락, 채널/큐 적체 같은 문제가 생길 수 있습니다. 특히 로컬툴이 내부적으로 고루틴/스레드를 쓴다면 누수와 데드락이 운영 장애로 이어집니다. 병렬 작업 디버깅 관점은 Go 고루틴 leak·채널 데드락 찾는 10가지 패턴도 참고할 만합니다.

흔한 실수 7가지

  1. 홈 디렉터리를 통째로 마운트
  2. .env 와 클라우드 크리덴셜을 컨테이너에 주입
  3. “쉘 문자열”을 그대로 실행
  4. allowlist 없이 네트워크 오픈
  5. 도구 설명에 과도한 권한/행동을 암시
  6. 출력/로그에 민감정보를 그대로 저장
  7. 샌드박스 없이 로컬에서 바로 실행(특히 개발 편의성 때문에)

결론: MCP는 연결 표준, 안전은 설계의 몫

MCP는 AutoGPT 같은 에이전트가 로컬툴을 호출하는 방식을 표준화해주지만, 보안과 운영 안정성은 별개의 문제입니다. 안전한 로컬툴 연결을 위해서는 다음의 우선순위를 추천합니다.

  • 도구를 작게 쪼개고 Read-only부터 시작
  • 경로/명령/네트워크를 allowlist로 제한
  • 컨테이너 기반 샌드박싱으로 호스트 영향 최소화
  • 모든 호출을 감사 가능하게 로깅
  • 고위험 동작은 사람 승인 게이트로 통제

이 다섯 가지를 지키면 “에이전트가 로컬에서 실제로 일하게 하되, 사고 반경은 작게” 만드는 현실적인 균형점을 만들 수 있습니다.