From 18d94d8913f0e217eb58135f7cf0a178d7b19b13 Mon Sep 17 00:00:00 2001
From: gpt-5-codex <elope@esus.name>
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
