From 18d94d8913f0e217eb58135f7cf0a178d7b19b13 Mon Sep 17 00:00:00 2001 From: gpt-5-codex Date: Fri, 10 Oct 2025 23:18:46 +0200 Subject: [PATCH] docs: provide hardened nginx and systemd templates --- examples/mcp.conf | 131 ++++++++++++++++++++++++++++++ examples/nginx-mcp-example.conf | 49 +++++++++++ systemd/mcp-browser-proxy.service | 42 ++++++++++ systemd/ssh-mcp-reverse.service | 48 +++++++++++ 4 files changed, 270 insertions(+) create mode 100644 examples/mcp.conf create mode 100644 examples/nginx-mcp-example.conf create mode 100644 systemd/mcp-browser-proxy.service create mode 100644 systemd/ssh-mcp-reverse.service diff --git a/examples/mcp.conf b/examples/mcp.conf new file mode 100644 index 0000000..f27547e --- /dev/null +++ b/examples/mcp.conf @@ -0,0 +1,131 @@ +# Example NGINX front-end for MCP Browser +# +# Replace the placeholder values (domain, certificate paths, slug, upstream +# ports) with values that match your deployment. This configuration exposes: +# * OAuth metadata + token endpoints implemented via an njs helper +# * An authenticated Streamable HTTP MCP endpoint +# * An unauthenticated debug endpoint (optional) that forwards to the +# mcp-browser SSE bridge +# +# The example assumes that: +# * An njs script lives at /etc/nginx/njs/mcp_oauth.js providing handlers +# for the metadata/register/token endpoints. +# * The mcp-browser bridge listens on 127.0.0.1:8251 for SSE requests. +# * The upstream MCP server (or proxy) exposes streamable-http at +# 127.0.0.1:8250. + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +js_path /etc/nginx/njs; +js_import mcp_oauth from /etc/nginx/njs/mcp_oauth.js; + +# Upstream pools (tune keepalive as needed) +upstream mcp_streamable_backend { + server 127.0.0.1:8250; + keepalive 32; +} + +upstream mcp_browser_backend { + server 127.0.0.1:8251; + keepalive 8; +} + +server { + listen 443 ssl http2; + server_name mcp.example.com; + + ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + # ssl_trusted_certificate /etc/letsencrypt/live/mcp.example.com/chain.pem; + + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + + # --- OAuth discovery + token endpoints (served by njs helper) ------------- + # Replace __MCP_SLUG__ with the slug you publish in OAuth metadata. + location = /.well-known/oauth-authorization-server/__MCP_SLUG__/mcp { + default_type application/json; + js_content mcp_oauth.metadata; + } + + location = /.well-known/oauth-protected-resource/__MCP_SLUG__/mcp { + default_type application/json; + js_content mcp_oauth.metadata; + } + + location = /.well-known/oauth-authorization-server { + default_type application/json; + js_content mcp_oauth.metadata; + } + + location = /.well-known/oauth-protected-resource { + default_type application/json; + js_content mcp_oauth.metadata; + } + + location = /register { + default_type application/json; + js_content mcp_oauth.register; + } + + location = /token { + default_type application/json; + js_content mcp_oauth.token; + } + + # Internal auth_request hook used by the protected MCP endpoint. + location = /_oauth_check { + internal; + js_content mcp_oauth.check; + } + + # --- Protected Streamable HTTP MCP endpoint -------------------------------- + # Requires successful OAuth check before proxying to the upstream server. + location ^~ /__MCP_SLUG__/mcp/ { + auth_request /_oauth_check; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Authorization ""; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Streaming friendly defaults + proxy_request_buffering off; + proxy_buffering off; + proxy_read_timeout 1d; + proxy_send_timeout 1d; + proxy_connect_timeout 30s; + + # Advertise latest protocol version if desired + # proxy_set_header MCP-Protocol-Version "2025-06-18"; + + proxy_pass http://mcp_streamable_backend; + } + + # --- Optional unauthenticated SSE bridge for local debugging --------------- + location ^~ /__MCP_SLUG__/mcp-browser/ { + proxy_http_version 1.1; + proxy_set_header Connection ""; + 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; + proxy_set_header X-NginX-Proxy true; + + proxy_request_buffering off; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 1d; + proxy_send_timeout 1d; + send_timeout 1d; + keepalive_timeout 1d; + + proxy_pass http://mcp_browser_backend/servers/mcp-browser/sse; + } +} diff --git a/examples/nginx-mcp-example.conf b/examples/nginx-mcp-example.conf new file mode 100644 index 0000000..7356e3b --- /dev/null +++ b/examples/nginx-mcp-example.conf @@ -0,0 +1,49 @@ +# Minimal NGINX reverse proxy for MCP Browser SSE bridge +# +# This configuration omits OAuth and exposes only the unauthenticated SSE +# endpoint. It is intended for quick local setup (e.g. tunnelling the bridge +# to the OpenAI MCP interface) and should not be used on the public Internet +# without adding authentication. + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream mcp_browser_backend { + server 127.0.0.1:8251; + keepalive 4; +} + +server { + listen 443 ssl http2; + server_name mcp-dev.example.com; + + ssl_certificate /etc/letsencrypt/live/mcp-dev.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/mcp-dev.example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + add_header X-Robots-Tag "noindex, nofollow, noarchive" always; + + # Replace __MCP_SLUG__ with your private slug value. + location ^~ /__MCP_SLUG__/mcp-browser/ { + proxy_http_version 1.1; + proxy_set_header Connection ""; + 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; + proxy_set_header X-NginX-Proxy true; + + proxy_request_buffering off; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 1d; + proxy_send_timeout 1d; + send_timeout 1d; + keepalive_timeout 1d; + + proxy_pass http://mcp_browser_backend/servers/mcp-browser/sse; + } +} diff --git a/systemd/mcp-browser-proxy.service b/systemd/mcp-browser-proxy.service new file mode 100644 index 0000000..9f883a7 --- /dev/null +++ b/systemd/mcp-browser-proxy.service @@ -0,0 +1,42 @@ +[Unit] +Description=Expose MCP Browser via HTTP bridge +Documentation=https://github.com/Xilope0/mcp-browser +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +Environment="PYTHONUNBUFFERED=1" +Environment="MCP_PROXY_BIN=%h/.local/bin/mcp-proxy" +Environment="MCP_PROXY_ALLOW_ORIGIN=https://platform.openai.com" +Environment="MCP_PROXY_PORT=14001" +Environment="MCP_PROXY_CONFIG=%h/.config/mcp-browser/proxy.yaml" +Environment="MCP_PROXY_EXTRA_ARGS=" +EnvironmentFile=-%h/.config/mcp-browser/proxy.env + +ExecStart=/usr/bin/env sh -c '\ + set -eu; \ + exec "$MCP_PROXY_BIN" \ + --allow-origin "$MCP_PROXY_ALLOW_ORIGIN" \ + --port "$MCP_PROXY_PORT" \ + --named-server-config "$MCP_PROXY_CONFIG" \ + $MCP_PROXY_EXTRA_ARGS' + +Restart=on-failure +RestartSec=5 +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=read-only +PrivateTmp=yes +ProtectControlGroups=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +RestrictSUIDSGID=yes +RestrictRealtime=yes +RestrictNamespaces=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 + +[Install] +WantedBy=default.target diff --git a/systemd/ssh-mcp-reverse.service b/systemd/ssh-mcp-reverse.service new file mode 100644 index 0000000..214fd22 --- /dev/null +++ b/systemd/ssh-mcp-reverse.service @@ -0,0 +1,48 @@ +[Unit] +Description=Maintain SSH reverse tunnel for MCP endpoints +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +Environment="MCP_TUNNEL_HOST=mcp.example.com" +Environment="MCP_TUNNEL_USER=mcp" +Environment="MCP_TUNNEL_KEY=%h/.ssh/mcp_reverse" +Environment="MCP_REMOTE_STREAMABLE_PORT=8250" +Environment="MCP_LOCAL_STREAMABLE_PORT=14000" +Environment="MCP_REMOTE_BROWSER_PORT=8251" +Environment="MCP_LOCAL_BROWSER_PORT=14001" +Environment="MCP_TUNNEL_EXTRA_ARGS=" +EnvironmentFile=-%h/.config/mcp-browser/tunnel.env + +ExecStart=/usr/bin/env sh -c '\ + set -eu; \ + exec /usr/bin/ssh -F /dev/null -i "$MCP_TUNNEL_KEY" -NT \ + -o ExitOnForwardFailure=yes \ + -o ServerAliveInterval=15 \ + -o ServerAliveCountMax=3 \ + -o StreamLocalBindUnlink=yes \ + -R 127.0.0.1:"$MCP_REMOTE_STREAMABLE_PORT":127.0.0.1:"$MCP_LOCAL_STREAMABLE_PORT" \ + -R 127.0.0.1:"$MCP_REMOTE_BROWSER_PORT":127.0.0.1:"$MCP_LOCAL_BROWSER_PORT" \ + $MCP_TUNNEL_EXTRA_ARGS \ + "$MCP_TUNNEL_USER@$MCP_TUNNEL_HOST"' + +Restart=always +RestartSec=5 +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=read-only +PrivateTmp=yes +ProtectControlGroups=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +RestrictSUIDSGID=yes +RestrictRealtime=yes +RestrictNamespaces=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +ReadOnlyPaths=%h/.ssh + +[Install] +WantedBy=default.target