feat: oauth refresh-token grant + per-runner subdomain TLS plumbing
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
OAUTH REFRESH-TOKEN
- oauth_tokens.subject column added (migration applied to prod DB): stores
the JWT sub claim from the original authorization so refreshes can
re-mint with the same identity without re-walking the (consumed) code.
- Authorization-code branch now writes subject AND uses a 30-day
expires_at for the row (was 1h — same as access token, which killed
refresh after 1h).
- New refresh_token grant branch:
* looks up token by refresh-hash + expiry
* client_id must match, client_secret verified if confidential
* RFC 8707: requested resource must equal stored resource
* OAuth 2.1 rotation: atomic UPDATE WHERE old_hash → new access JWT,
new refresh token, extended expiry; loser of a race sees invalid_grant
- Access TTL (1h) and refresh TTL (30d) extracted as constants.
Clients no longer have to re-authorize hourly. Closes Zb-001.
PER-RUNNER SUBDOMAIN TLS (Z1-002)
Code path:
- New MCP_DOMAIN env (e.g. "mcp.buildmymcpserver.com") + RUNNER_MAP_DIR
(default /var/runner-map) in generator config.
- deployContainer: writes /var/runner-map/<slug>.conf with content
"slug.MCP_DOMAIN port;" and computes publicUrl as
https://<slug>.<MCP_DOMAIN>. Falls back to http://host:port when
MCP_DOMAIN is unset (zero behaviour change until host is configured).
- stopContainer (both api/lib/docker.ts and generator/lib/deploy.ts) now
accepts an optional slug arg and removes the map fragment. Callers
(DELETE /v1/servers/:id, admin template takedown) updated.
Infra path (one-time host setup — Marco runs as root):
- scripts/setup-runner-tls.sh:
1. nginx vhost matching *.mcp.buildmymcpserver.com via regex →
reads slug→port from /opt/buildmymcpserver/runner-map.combined
2. systemd inotify service watches the map dir, combines fragments
on any change, reloads nginx
3. installs inotify-tools if missing, idempotent
- Prereqs documented at top: Cloudflare wildcard DNS proxied, Origin CA
cert for *.mcp.buildmymcpserver.com, SSL mode Full (strict).
- After running: edit docker-compose.prod.yml to mount the map dir into
api + generator, set MCP_DOMAIN in env, recreate containers.
Closes Zb-001 fully. Closes Z1-002 on the code side; one Marco-on-host
action away from closing it on the infra side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
165
scripts/setup-runner-tls.sh
Normal file
165
scripts/setup-runner-tls.sh
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# setup-runner-tls.sh
|
||||
#
|
||||
# One-time host setup for per-runner subdomain TLS. Run as root on the BMM
|
||||
# host AFTER you've done the prereqs below. Idempotent — safe to re-run.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Creates /opt/buildmymcpserver/runner-map/ (volume-mounted into bmm-api
|
||||
# and bmm-generator — they drop one .conf fragment per live runner)
|
||||
# 2. Installs an nginx vhost that catches *.mcp.buildmymcpserver.com,
|
||||
# reads slug→port from a combined map file, and reverse-proxies to the
|
||||
# runner on localhost
|
||||
# 3. Installs a systemd service that inotify-watches the map dir, combines
|
||||
# all fragments into a single map file, and reloads nginx on any change
|
||||
#
|
||||
# After this script:
|
||||
# - In docker-compose.prod.yml, add a volume mount to BOTH api and generator:
|
||||
# volumes:
|
||||
# - /opt/buildmymcpserver/runner-map:/var/runner-map
|
||||
# - In .env.production, add:
|
||||
# MCP_DOMAIN=mcp.buildmymcpserver.com
|
||||
# - docker compose up -d --force-recreate api generator
|
||||
# - From now on every deployed runner gets https://<slug>.mcp.buildmymcpserver.com
|
||||
#
|
||||
# ─── PREREQS (do these in Cloudflare dashboard before running) ───────────
|
||||
# A. DNS: Add an A-record '*.mcp.buildmymcpserver.com' → 213.239.213.217
|
||||
# Proxy status: Proxied (orange cloud)
|
||||
# B. SSL: Cloudflare → SSL/TLS → Origin Server → Create Certificate
|
||||
# Hostnames: *.mcp.buildmymcpserver.com, mcp.buildmymcpserver.com
|
||||
# Save the .crt and .key to:
|
||||
# /etc/ssl/buildmymcpserver/mcp-runners.crt (mode 644)
|
||||
# /etc/ssl/buildmymcpserver/mcp-runners.key (mode 600, root:root)
|
||||
# C. SSL mode: Cloudflare → SSL/TLS → Overview → set to "Full (strict)"
|
||||
# (you've likely already set this for api.* — same setting)
|
||||
#
|
||||
# Run: sudo bash scripts/setup-runner-tls.sh
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Run as root (sudo bash $0)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAP_DIR="/opt/buildmymcpserver/runner-map"
|
||||
COMBINED="/opt/buildmymcpserver/runner-map.combined"
|
||||
VHOST_DST="/etc/nginx/sites-available/bmm-mcp-runners"
|
||||
VHOST_LNK="/etc/nginx/sites-enabled/bmm-mcp-runners"
|
||||
CERT="/etc/ssl/buildmymcpserver/mcp-runners.crt"
|
||||
KEY="/etc/ssl/buildmymcpserver/mcp-runners.key"
|
||||
|
||||
echo "─── checking prereqs ───────────────────────────────────────"
|
||||
for f in "$CERT" "$KEY"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "MISSING: $f — see PREREQS at the top of this script."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! command -v inotifywait >/dev/null; then
|
||||
echo "Installing inotify-tools…"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq inotify-tools
|
||||
fi
|
||||
|
||||
echo "─── creating map dir + initial combined file ──────────────"
|
||||
mkdir -p "$MAP_DIR"
|
||||
chmod 755 "$MAP_DIR"
|
||||
touch "$COMBINED"
|
||||
chmod 644 "$COMBINED"
|
||||
|
||||
echo "─── writing nginx vhost ──────────────────────────────────"
|
||||
cat > "$VHOST_DST" <<'NGINX'
|
||||
# BMM per-runner subdomain proxy. Map file (slug→port) is regenerated by
|
||||
# the bmm-api and bmm-generator containers; a systemd inotify watcher
|
||||
# combines them into the included file and runs `nginx -s reload`.
|
||||
|
||||
map $http_host $bmm_runner_port {
|
||||
default 0;
|
||||
include /opt/buildmymcpserver/runner-map.combined;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name ~^(?<bmm_slug>[a-z0-9][a-z0-9-]*)\.mcp\.buildmymcpserver\.com$;
|
||||
|
||||
ssl_certificate /etc/ssl/buildmymcpserver/mcp-runners.crt;
|
||||
ssl_certificate_key /etc/ssl/buildmymcpserver/mcp-runners.key;
|
||||
|
||||
client_max_body_size 4M;
|
||||
|
||||
# Unknown slugs land here — return 404 instead of a confusing default vhost.
|
||||
if ($bmm_runner_port = 0) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:$bmm_runner_port;
|
||||
proxy_http_version 1.1;
|
||||
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 https;
|
||||
# MCP uses Streamable HTTP — disable buffering so response chunks flow.
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
ln -sf "$VHOST_DST" "$VHOST_LNK"
|
||||
|
||||
echo "─── writing systemd watcher service ──────────────────────"
|
||||
cat > /etc/systemd/system/bmm-runner-map.service <<EOF
|
||||
[Unit]
|
||||
Description=BMM runner-map combiner + nginx reload
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStartPre=/bin/bash -c 'cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload || true'
|
||||
ExecStart=/bin/bash -c 'while inotifywait -q -e create,modify,delete,moved_to,moved_from ${MAP_DIR}; do cat ${MAP_DIR}/*.conf 2>/dev/null > ${COMBINED} || true; /usr/sbin/nginx -t && /usr/sbin/nginx -s reload; done'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now bmm-runner-map
|
||||
|
||||
echo "─── verifying nginx config + reload ──────────────────────"
|
||||
nginx -t
|
||||
nginx -s reload || true
|
||||
|
||||
echo ""
|
||||
echo "─── DONE ─────────────────────────────────────────────────"
|
||||
echo ""
|
||||
echo "Next steps (one-time):"
|
||||
echo ""
|
||||
echo "1) Edit /opt/buildmymcpserver/docker-compose.prod.yml — add to BOTH"
|
||||
echo " the 'api' and 'generator' services:"
|
||||
echo ""
|
||||
echo " volumes:"
|
||||
echo " - /opt/buildmymcpserver/runner-map:/var/runner-map"
|
||||
echo ""
|
||||
echo "2) Edit /opt/buildmymcpserver/.env.production — add:"
|
||||
echo ""
|
||||
echo " MCP_DOMAIN=mcp.buildmymcpserver.com"
|
||||
echo ""
|
||||
echo "3) Restart api + generator so they pick up the env + volume:"
|
||||
echo ""
|
||||
echo " cd /opt/buildmymcpserver"
|
||||
echo " docker compose --env-file .env.production -f docker-compose.prod.yml \\"
|
||||
echo " up -d --force-recreate api generator"
|
||||
echo ""
|
||||
echo "Test (after at least one runner has been deployed):"
|
||||
echo " curl -I https://<slug>.mcp.buildmymcpserver.com/health"
|
||||
echo ""
|
||||
echo "If you ever need to verify the map state:"
|
||||
echo " cat ${COMBINED}"
|
||||
echo " systemctl status bmm-runner-map"
|
||||
Reference in New Issue
Block a user