Supergateway 를 사용한 MCP 서버 보안 강화 — 엔터프라이즈 완전 가이드

 

Supergateway는 MCP stdio 서버를 네트워크로 노출하는 프로토콜 변환기이지만, 보안 기능은 내장되어 있지 않습니다. 프로덕션 환경에서는 반드시 외부 보안 계층과 결합해야 합니다.

🎯 보안 아키텍처 한눈에

┌─────────────────────────────────────────────────────────────────┐
│ Claude Desktop / Cursor (클라이언트) │
│ "어제 #프로젝트 채널 요약해줘" │
│ ↓ (HTTPS + JWT/Bearer Token) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Reverse Proxy (Nginx / Caddy / Kong) │ │
│ │ - TLS 종단 (HTTPS) │ │
│ │ - JWT/OAuth 토큰 검증 │ │
│ │ - Rate Limiting (초당 요청 수 제한) │ │
│ │ - IP 화이트리스트 │ │
│ │ - 감사 로그 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ (내부망, 인증 완료된 트래픽만) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Supergateway (--oauth2Bearer 또는 --header) │ │
│ │ - 인증 토큰을 백엔드 MCP 서버로 전달 │ │
│ │ - 프로토콜 변환 (SSE ↔ stdio) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MCP Slack Server (stdio) │ │
│ │ - Slack API 연동 │ │
│ │ - 토큰 검증 로직 (선택) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ Slack API │
└─────────────────────────────────────────────────────────────────┘

🔐 5 단계 보안 강화 전략

1️⃣ TLS 암호화 (필수) — 모든 트래픽 HTTPS 로

Supergateway 는 기본적으로 평문 HTTP를 사용합니다. 프로덕션에서는 반드시 TLS 종단이 필요합니다.

방법 A: Nginx 역방향 프록시 (권장)

Nginx 설치 (Ubuntu 예시):

sudo apt update && sudo apt install nginx -y

Nginx 설정 (/etc/nginx/sites-available/mcp-gateway):

server {
 listen 80;
 server_name mcp.yourcompany.com;
 return 301 https://$server_name$request_uri;
}

server {
 listen 443 ssl http2;
 server_name mcp.yourcompany.com;

 # TLS 인증서 (Let's Encrypt 권장)
 ssl_certificate /etc/letsencrypt/live/mcp.yourcompany.com/fullchain.pem;
 ssl_certificate_key /etc/letsencrypt/live/mcp.yourcompany.com/privkey.pem;

 # 현대적 TLS 설정
 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_prefer_server_ciphers on;
 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
 ssl_session_cache shared:SSL:10m;
 ssl_session_timeout 10m;

 # 보안 헤더
 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 add_header X-Content-Type-Options nosniff always;
 add_header X-Frame-Options DENY always;
 add_header X-XSS-Protection "1; mode=block" always;

 # Rate Limiting 영역
 limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=10r/s;

 location / {
 # Rate Limiting 적용 (초당 10 요청, 버스트 20)
 limit_req zone=mcp_limit burst=20 nodelay;

 # Supergateway 로 프록시
 proxy_pass http://localhost:3000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;

 # SSE/WebSocket 타임아웃
 proxy_read_timeout 86400s;
 proxy_send_timeout 86400s;
 }

 # 건강 점검 엔드포인트 (모니터링용)
 location /health {
 access_log off;
 return 200 "healthy\n";
 add_header Content-Type text/plain;
 }
}

설정 활성화:

sudo ln -s /etc/nginx/sites-available/mcp-gateway /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Let’s Encrypt 인증서 발급:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d mcp.yourcompany.com

방법 B: Caddy (자동 TLS — 가장 간단)

Caddyfile (/etc/caddy/Caddyfile):

mcp.yourcompany.com {
 reverse_proxy localhost:3000

 # 보안 헤더
 header {
 Strict-Transport-Security "max-age=31536000; includeSubDomains"
 X-Content-Type-Options nosniff
 X-Frame-Options DENY
 }

 # Rate Limiting (Caddy 플러그인 필요)
 @ratelimit {
 rate 10r/s
 burst 20
 }
 respond @ratelimit "Too Many Requests" 429
}

장점: 인증서 발급·갱신 완전 자동 (Zero-Config TLS).

2️⃣ 인증 (Authentication) — 토큰 없는 접근 차단

전략 A: Nginx + Basic Auth (간단한 팀용)

Nginx 설정 추가:

location / {
 # Basic Auth
 auth_basic "MCP Gateway";
 auth_basic_user_file /etc/nginx/.htpasswd;

 # ... 나머지 프록시 설정
 proxy_pass http://localhost:3000;
}

사용자 생성:

sudo apt install apache2-utils -y
sudo htpasswd -c /etc/nginx/.htpasswd admin
# 비밀번호 입력 프롬프트

단점: 평문 비밀번호 전송 (반드시 HTTPS 와 함께 사용).

전략 B: Nginx + JWT 검증 (엔터프라이즈 권장)

JWT 검증 모듈 설치 (nginx-plus 또는 openresty):

# Ubuntu (OpenResty)
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt install -y openresty

Nginx 설정 (/etc/nginx/sites-available/mcp-gateway):

http {
 # JWT 검증 (lua-resty-openidc 사용)
 lua_package_path "/usr/share/lua/5.1/?.lua;;";

 server {
 listen 443 ssl;
 server_name mcp.yourcompany.com;

 # ... TLS 설정 ...

 location / {
 # JWT 검증
 access_by_lua_block {
 local openidc = require("resty.openidc")
 local res, err = openidc.authenticate({
 discovery = "https://auth.yourcompany.com/.well-known/openid-configuration",
 client_id = "mcp-gateway",
 client_secret = "your-client-secret",
 redirect_uri = "https://mcp.yourcompany.com/callback",
 scope = "openid profile mcp.access",
 ssl_verify = "yes"
 })

 if err then
 ngx.status = 401
 ngx.say("Unauthorized: ", err)
 ngx.exit(401)
 end

 # 검증된 JWT 정보를 헤더로 전달
 ngx.req.set_header("X-User-Email", res.user.email)
 ngx.req.set_header("X-User-Roles", res.user.roles)
 }

 # Supergateway 로 프록시 (JWT 정보 포함)
 proxy_pass http://localhost:3000;
 proxy_set_header X-User-Email $http_x_user_email;
 proxy_set_header X-User-Roles $http_x_user_roles;
 }
 }
}

Supergateway 실행 (JWT 토큰을 백엔드로 전달):

npx -y supergateway \
 --stdio "python mcp-slack-server.py" \
 --port 3000 \
 --header "X-Auth-Source: nginx-jwt"

전략 C: Supergateway 직접 인증 (–oauth2Bearer)

Supergateway 실행 (간단한 Bearer 토큰 검증):

npx -y supergateway \
 --stdio "python mcp-slack-server.py" \
 --port 3000 \
 --oauth2Bearer "sk-mcp-your-secret-token-xyz123"

클라이언트 설정 (Claude Desktop):

{
 "mcpServers": {
 "slack-secure": {
 "command": "npx",
 "args": }
 }
}

주의: 이 방식은 토큰이 클라이언트 설정 파일에 평문으로 저장됨. 팀 배포 시 .env 파일 + 시크릿 관리자 사용 권장.

3️⃣ 인가 (Authorization) — 사용자별 접근 제어

Nginx + JWT Claims 기반 RBAC

Nginx 설정 (역할별 접근 제어):

location / {
 # JWT 검증 (위와 동일)
 access_by_lua_block {
 local openidc = require("resty.openidc")
 local res, err = openidc.authenticate({...})

 if err then
 ngx.exit(401)
 end

 # 역할 추출
 local roles = res.user.roles or ""
 ngx.req.set_header("X-User-Roles", roles)

 # 관리자만 접근 가능한 엔드포인트 차단
 if string.match(ngx.var.uri, "/admin") and not string.match(roles, "admin") then
 ngx.status = 403
 ngx.say("Forbidden: Admin access required")
 ngx.exit(403)
 end
 }

 proxy_pass http://localhost:3000;
}

MCP 서버 내부 인가 (고급)

MCP 서버 코드에서 JWT 헤더를 검증하여 도구별 접근 제어 구현:

# mcp-slack-server.py
import os
from mcp.server import Server
from mcp.server.stdio import stdio_server

server = Server("slack-secure")

@server.tool("send_slack_message")
async def send_message(channel: str, message: str):
 # JWT 헤더에서 역할 추출
 auth_header = os.environ.get("HTTP_X_USER_ROLES", "")

 # 관리자만 메시지 발송 가능
 if "admin" not in auth_header:
 raise PermissionError("send_slack_message requires admin role")

 # 실제 Slack API 호출
 from slack_sdk import WebClient
 client = WebClient(token=os.environ)
 return client.chat_postMessage(channel=channel, text=message)

@server.tool("read_slack_messages")
async def read_messages(channel: str, limit: int = 10):
 # 모든 인증된 사용자는 읽기 가능
 from slack_sdk import WebClient
 client = WebClient(token=os.environ)
 return client.conversations_history(channel=channel, limit=limit)

if __name__ == "__main__":
 import asyncio
 asyncio.run(stdio_server(server))

4️⃣ Rate Limiting — 남용 및 비용 폭주 방지

MCP 도구 호출은 비용이 많이 드는 LLM 연산을 트리거할 수 있습니다. Rate Limiting 은 필수입니다.

Nginx 기본 Rate Limiting

Nginx 설정:

http {
 # 전역 Rate Limiting 존 정의 (초당 10 요청)
 limit_req_zone $binary_remote_addr zone=mcp_global:10m rate=10r/s;

 # 사용자별 Rate Limiting (JWT email 기준)
 limit_req_zone $http_x_user_email zone=mcp_per_user:10m rate=5r/s;

 server {
 location / {
 # 전역 제한 + 사용자별 제한 중첩 적용
 limit_req zone=mcp_global burst=20 nodelay;
 limit_req zone=mcp_per_user burst=10 nodelay;

 # 제한 초과 시 응답
 limit_req_status 429;
 limit_req_log_level warn;

 proxy_pass http://localhost:3000;
 }

 # 건강 점검은 Rate Limiting 제외
 location /health {
 limit_req off;
 return 200 "healthy\n";
 }
 }
}

도구별 Rate Limiting (고급)

비싼 도구 (예: trigger_long_operation) 는 더 엄격하게 제한:

Nginx + Lua:

location / {
 access_by_lua_block {
 local body = ngx.req.get_body_data()
 if body then
 local json = require("cjson")
 local data = json.decode(body)
 local tool_name = data.params and data.params.name

 -- 비싼 도구는 더 엄격하게 제한
 if tool_name == "trigger_long_operation" then
 local key = "tool:" .. tool_name .. ":user:" .. (ngx.var.http_x_user_email or "anonymous")
 local count = ngx.shared.dict:get(key) or 0
 if count >= 3 then
 ngx.status = 429
 ngx.say("Rate limit exceeded for tool: " .. tool_name)
 ngx.exit(429)
 end
 ngx.shared.dict:incr(key, 1, 0)
 ngx.shared.dict:expire(key, 60) -- 1 분 창
 end
 end
 }

 proxy_pass http://localhost:3000;
}

5️⃣ 감사 로그 & 모니터링 — 이상 탐지 및 규정 준수

Nginx 접근 로그 (확장 형식)

Nginx 설정:

http {
 # MCP 특화 로그 형식
 log_format mcp_json escape=json
 '{'
 '"time":"$time_iso8601",'
 '"client_ip":"$remote_addr",'
 '"user_email":"$http_x_user_email",'
 '"method":"$request_method",'
 '"uri":"$uri",'
 '"status":$status,'
 '"bytes":$body_bytes_sent,'
 '"referer":"$http_referer",'
 '"user_agent":"$http_user_agent",'
 '"request_time":$request_time'
 '}';

 server {
 access_log /var/log/nginx/mcp-access.log mcp_json;
 error_log /var/log/nginx/mcp-error.log warn;

 # ... 나머지 설정
 }
}

로그 분석 예시 (ELK 스택 또는 Datadog 연동):

# 최근 1 시간 동안 429 에러가 많은 사용자 top 5
cat /var/log/nginx/mcp-access.log | \
 jq 'select(.status == 429) | .user_email' | \
 sort | uniq -c | sort -rn | head -5

건강 점검 & 생존_probe

Nginx 설정:

location /health {
 access_log off;

 # Supergateway 건강 상태 확인
 proxy_pass http://localhost:3000/health;
 proxy_read_timeout 5s;

 # 실패 시 503 반환
 proxy_intercept_errors on;
 error_page 502 503 504 = @health_fail;
}

location @health_fail {
 return 503 "MCP Gateway unhealthy\n";
 add_header Content-Type text/plain;
}

Kubernetes 생존_probe (예시):

livenessProbe:
 httpGet:
 path: /health
 port: 443
 scheme: HTTPS
 initialDelaySeconds: 30
 periodSeconds: 10
 timeoutSeconds: 5
 failureThreshold: 3

📊 보안 설정 완전 예시 (프로덕션)

전체 아키텍처

Internet
 ↓ (HTTPS 443)
┌─────────────────────────────────────┐
│ Cloudflare / AWS WAF │
│ - DDoS 방어 │
│ - IP 평판 기반 차단 │
│ - 지리적 차단 (선택) │
└─────────────────────────────────────┘
 ↓
┌─────────────────────────────────────┐
│ Nginx (Reverse Proxy) │
│ - TLS 종단 (Let's Encrypt) │
│ - JWT/OAuth 검증 │
│ - Rate Limiting (전역 + 사용자별) │
│ - 감사 로그 │
└─────────────────────────────────────┘
 ↓ (내부망, 인증 완료)
┌─────────────────────────────────────┐
│ Supergateway │
│ --oauth2Bearer 토큰 전달 │
│ --stdio MCP Slack Server │
└─────────────────────────────────────┘
 ↓
┌─────────────────────────────────────┐
│ MCP Slack Server (stdio) │
│ - Slack API 연동 │
│ - 도구별 인가 (선택) │
└─────────────────────────────────────┘

1. Supergateway 실행 스크립트 (/opt/mcp/start.sh)

#!/bin/bash
set -e

# 환경 변수 로드
source /opt/mcp/.env

# Supergateway 시작 (systemd 서비스용)
exec npx -y supergateway \
 --stdio "python /opt/mcp/mcp-slack-server.py" \
 --port 3000 \
 --outputTransport sse \
 --header "X-Slack-Token: $SLACK_BOT_TOKEN" \
 --header "X-Auth-Source: nginx" \
 2>&1 | tee /var/log/mcp/supergateway.log

실행 권한 부여:

sudo chmod +x /opt/mcp/start.sh

2. systemd 서비스 등록 (/etc/systemd/system/mcp-gateway.service)


Description=MCP Slack Gateway (Supergateway)
After=network.target

Type=simple
User=mcp
Group=mcp
WorkingDirectory=/opt/mcp
EnvironmentFile=/opt/mcp/.env
ExecStart=/opt/mcp/start.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mcp-gateway

# 보안 강화
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/mcp

WantedBy=multi-user.target

서비스 시작:

sudo systemctl daemon-reload
sudo systemctl enable mcp-gateway
sudo systemctl start mcp-gateway
sudo systemctl status mcp-gateway

3. Nginx 최종 설정 (/etc/nginx/sites-available/mcp-gateway)

# 전역 Rate Limiting 존
limit_req_zone $binary_remote_addr zone=mcp_global:10m rate=10r/s;
limit_req_zone $http_x_user_email zone=mcp_per_user:10m rate=5r/s;

# JWT 검증용 Lua 스크립트 경로
lua_package_path "/usr/share/lua/5.1/?.lua;;";

server {
 listen 80;
 server_name mcp.yourcompany.com;
 return 301 https://$server_name$request_uri;
}

server {
 listen 443 ssl http2;
 server_name mcp.yourcompany.com;

 # TLS
 ssl_certificate /etc/letsencrypt/live/mcp.yourcompany.com/fullchain.pem;
 ssl_certificate_key /etc/letsencrypt/live/mcp.yourcompany.com/privkey.pem;
 ssl_protocols TLSv1.2 TLSv1.3;
 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

 # 보안 헤더
 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
 add_header X-Content-Type-Options nosniff always;
 add_header X-Frame-Options DENY always;

 # MCP 특화 로그
 access_log /var/log/nginx/mcp-access.log mcp_json;
 error_log /var/log/nginx/mcp-error.log warn;

 location / {
 # JWT 검증 (Keycloak 예시)
 access_by_lua_block {
 local openidc = require("resty.openidc")
 local res, err = openidc.authenticate({
 discovery = "https://auth.yourcompany.com/.well-known/openid-configuration",
 client_id = "mcp-gateway",
 client_secret = os.getenv("KEYCLOAK_CLIENT_SECRET"),
 redirect_uri = "https://mcp.yourcompany.com/callback",
 scope = "openid profile mcp.access",
 ssl_verify = "yes"
 })

 if err then
 ngx.status = 401
 ngx.say("{\"error\": \"Unauthorized\", \"message\": \"" .. err .. "\"}")
 ngx.exit(401)
 end

 -- 사용자 정보 헤더로 전달
 ngx.req.set_header("X-User-Email", res.user.email)
 ngx.req.set_header("X-User-Roles", table.concat(res.user.roles or {}, ","))
 }

 # Rate Limiting
 limit_req zone=mcp_global burst=20 nodelay;
 limit_req zone=mcp_per_user burst=10 nodelay;
 limit_req_status 429;

 # Supergateway 로 프록시
 proxy_pass http://localhost:3000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;

 # SSE/WebSocket 타임아웃
 proxy_read_timeout 86400s;
 proxy_send_timeout 86400s;

 # 에러 처리
 proxy_intercept_errors on;
 error_page 502 503 504 = @gateway_error;
 }

 location @gateway_error {
 default_type application/json;
 return 503 '{"error": "Service Unavailable", "message": "MCP Gateway is temporarily unavailable"}';
 }

 location /health {
 access_log off;
 proxy_pass http://localhost:3000/health;
 proxy_read_timeout 5s;
 add_header Content-Type text/plain;
 }

 location /metrics {
 # Prometheus 용 메트릭 엔드포인트 (내부망만 접근)
 allow 10.0.0.0/8;
 allow 172.16.0.0/12;
 deny all;
 proxy_pass http://localhost:3000/metrics;
 }
}

4. 환경 변수 관리 (/opt/mcp/.env)

# 절대 Git 에 커밋하지 말 것!
SLACK_BOT_TOKEN=xoxb-123456789012-...
KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret
MCP_AUTH_TOKEN=sk-mcp-your-secret-token-xyz123

권한 설정:

sudo chown mcp:mcp /opt/mcp/.env
sudo chmod 600 /opt/mcp/.env

🔍 보안 검증 체크리스트

배포 전 검증

  • HTTPS 강제http:// 접근 시 301 리다이렉트 확인
  • TLS 버전ssl_protocols TLSv1.2 TLSv1.3; 설정 확인
  • 인증 테스트: 토큰 없이 접근 시 401 Unauthorized 반환 확인
  • Rate Limiting: 초당 10 회 이상 요청 시 429 Too Many Requests 확인
  • 감사 로그/var/log/nginx/mcp-access.log 에 JSON 로그 기록 확인
  • 건강 점검/health 엔드포인트에서 200 OK 반환 확인

정기 감사 (월 1 회)

  • 토큰 순환: SLACK_BOT_TOKEN, MCP_AUTH_TOKEN 90 일마다 재발급
  • 로그 분석: 이상한 패턴 (예: 특정 사용자의 급증한 요청) 탐지
  • 권한 검토: JWT 역할 (roles) 이 최소 권한 원칙 준수하는지 확인
  • 보안 업데이트: Nginx, Supergateway, MCP 서버 최신 버전으로 업데이트

⚠️ 절대 하지 말아야 할 실수

❌ 실수 ✅ 올바른 방법
Supergateway 를 직접 인터넷에 노출 (포트 3000 개방) 반드시 Nginx/Caddy 역방향 프록시 뒤에 배치
토큰을 설정 파일에 평문으로 기록 환경 변수 또는 시크릿 관리자 (AWS Secrets Manager, HashiCorp Vault) 사용
Rate Limiting 없이 무제한 허용 전역 + 사용자별 이중 Rate Limiting 필수
HTTP 로 토큰 전송 HTTPS 필수 (TLS 1.2+)
감사 로그를 남기지 않음 JSON 형식으로 상세 로그 기록 + 중앙 집중식 수집 (ELK, Datadog)
모든 사용자에게 관리자 권한 부여 JWT Claims 기반 RBAC으로 도구별 접근 제어

📈 보안 강화 후 기대 효과

지표 강화 전 강화 후
무단 접근 시도 차단 불가 100% 차단 (JWT 검증)
DDoS 내성 취약 초당 10 요청 제한 (Rate Limiting)
데이터 유출 위험 평문 전송 TLS 암호화 (HTTPS)
규정 준수 어려움 감사 로그로 GDPR/HIPAA 대응 가능
비용 폭주 무제한 호출 사용자별 할당량으로 LLM 비용 통제

“Supergateway 는 도구일 뿐, 보안은 당신의 책임입니다.”

3 시간 투자로 엔터프라이즈급 보안을 구축하세요. MCP 서버가 다루는 데이터는 회사의 신경계입니다.

댓글 남기기