mcp-stepup-gateway
mcp-stepup-gateway
MCP-шлюз, который требует passkey (WebAuthn) по запросу — «step-up auth» — прежде чем разрешить удалённому клиенту (Claude.ai, через Custom Connector) читать или писать в хранилище Obsidian, защищённое enquire-mcp. Вход через Google и allowlist (как в mcp-oauth-gateway) решают, кто может подключаться; этот проект решает, для каждого инструмента, что этот человек может делать без повторного подтверждения личности, а что требует свежего касания passkey.
Он родился из конкретного случая: mcp-oauth-gateway/enquire-mcp-gateway уже решают «аутентифицировать того, кто подключается» (OAuth + allowlist). Не хватало второго уровня: даже внутри allowlist не каждый tool call должен быть одинаково свободным. Прочитать заметку — дёшево; удалить или переписать содержимое хранилища через LLM, которая может находиться под prompt injection, — нет. Этот шлюз добавляет это различие, не трогая сам enquire-mcp.
Зачем это нужно
Удалённый MCP-клиент, аутентифицированный через OAuth, с точки зрения хранилища всё ещё является «LLM с полным доступом». Это проблема в двух аспектах:
LLM можно манипулировать. Вредоносное содержимое в заметке или в ответе инструмента может попытаться указать агенту удалить или перезаписать что-то — prompt injection не гипотетичен.
«Аутентифицированный один раз» не должно означать «авторизованный навсегда». Длительная OAuth-сессия не должна давать одной и той же LLM неограниченное право записи бессрочно, без свежего доказательства присутствия человека.
Решение здесь — модель уровней риска для каждого инструмента с короткоживущим handle способности (15 мин), который авторизует чтение, и подтверждением passkey на каждый вызов, которое авторизует любую запись или удаление — формируемым на основе реальных аргументов, полученных сервером, а не текста, контролируемого LLM.
Related MCP server: Obsidian MCP Wrapper
Архитектура
Cliente MCP remoto (Claude.ai, via Custom Connector)
│ HTTPS (OAuth Google + allowlist -- fora do escopo deste
│ README; ver mcp-oauth-gateway/enquire-mcp-gateway)
▼
┌───────────────────────────────────────────────────────────┐
│ gateway │
│ │
│ StepUpMiddleware -- por tool call: │
│ 1. policy.yaml decide o nivel (0/1/2) da tool │
│ 2. L0 (tools de auth) -- sempre passa │
│ 3. L1 (leitura) -- exige handle de sessao valido │
│ (senao devolve AUTH_REQUIRED + URL de unlock) │
│ 4. L2 (escrita/delete) -- exige confirmacao fresca │
│ por chamada (args_digest HMAC liga a aprovacao aos │
│ argumentos EXATOS; senao devolve CONFIRMATION_REQUIRED) │
│ │
│ Tools injetadas (nivel 0, sempre disponiveis): │
│ vault_auth_unlock / vault_auth_check / vault_auth_status │
└──────────────────────────┬───────────────────────────────────┘
│ Streamable HTTP + bearer
▼
┌───────────────────────────────────────────────────────────┐
│ auth-service │
│ │
│ WebAuthn (passkey) -- registro, challenges de unlock e de │
│ confirmacao, sessoes (SQLite), audit log append-only. │
│ So alcancavel via rotas /internal (X-Gateway-Key) do │
│ gateway, ou pelas telas publicas /unlock, /confirm, │
│ /register (esta ultima so com token de bootstrap). │
└──────────────────────────┬───────────────────────────────────┘
│ nunca fala com o backend
│ diretamente -- so autentica
▼
(o handle/token volta ao Claude via
gateway, que entao repassa a chamada
original ao backend)
│
▼
┌───────────────────────────────────────────────────────────┐
│ backend │
│ enquire-mcp (serve-http, vault Obsidian) │
└───────────────────────────────────────────────────────────┘gateway никогда не хранит никаких учётных данных — он только обращается к auth-service (внутренние маршруты, аутентифицируемые через GATEWAY_KEY) с вопросами «этот handle авторизует этот инструмент?» или «это подтверждение одобрило именно эти аргументы?». Человеку никогда не нужно вводить или вставлять что-либо в чат: вся процедура passkey происходит в браузере, по URL, который обслуживает auth-service.
Уровни риска
Уровень | Что требуется | Пример |
L0 | Ничего — всегда разрешено |
|
L1 | Действительный handle сессии (абсолютный TTL 15 мин, idle 5 мин) |
|
L2 | Подтверждение passkey на каждый вызов, привязанное к точным аргументам через |
|
policies/policy.yaml сопоставляет каждый инструмент бэкенда с уровнем. Deny-by-default: любой инструмент, не сопоставленный явно, попадает на самый ограничительный уровень (default_level: 2) — если enquire-mcp получит новый инструмент в обновлении (бэкенд запускается через npx -y, поэтому версия может измениться при любом подъёме), он появится защищённым, а не открытым. Смотрите комментарии в самом policies/policy.yaml о происхождении используемых имён инструментов и о том, что ещё нужно проверить вживую перед продакшеном.
Настройка
Требуются Docker и Docker Compose. Три сервиса (gateway, auth-service, backend) поднимаются вместе.
1. Переменные окружения
cp .env.example .env # Windows: Copy-Item .env.example .envЗаполните в корне репозитория:
Google OAuth (
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PUBLIC_BASE_URL,ALLOWED_EMAILS) — тот же шаблон, что и вmcp-oauth-gateway; см. README того проекта с пошаговой инструкцией по созданию OAuth Client в Google Cloud Console.WebAuthn (
WEBAUTHN_RP_ID,WEBAUTHN_RP_NAME,PUBLIC_ORIGIN,GATEWAY_KEY,DIGEST_KEY) — см. предупреждение ниже перед установкойWEBAUTHN_RP_ID. СгенерируйтеGATEWAY_KEYиDIGEST_KEYс помощьюopenssl rand -hex 32.Backend (
BACKEND_BEARER_TOKEN,OBSIDIAN_VAULT_PATH) — общий токен междуgatewayиbackendи путь на хосте к защищаемому хранилищу Obsidian.
WEBAUTHN_RP_ID— ПОСТОЯННАЯ. Это домен (без порта и протокола), который встраивается в саму подпись WebAuthn каждой зарегистрированной passkey. Изменение этого значения после первой регистрации инвалидирует ВСЕ passkey — всем придётся регистрироваться заново, с новым bootstrap. Определите окончательный домен (тот же хост, что иPUBLIC_BASE_URL, безhttps://) до регистрации первой passkey, а не после.auth-serviceотказывается запускаться без этой переменной (src/authsvc/config.py) — намеренно: молчаливый default здесь был бы хуже, чем падение при загрузке.
2. Поднять стек
docker compose --env-file .env -f docker/docker-compose.yml up -d --build
docker compose --env-file .env -f docker/docker-compose.yml logs -f auth-service--env-file .env не опционально — Docker Compose разрешает ${VAR} из compose относительно каталога самого файла (docker/), а не корня репозитория. Запуск без этого флага приводит к тому, что OBSIDIAN_VAULT_PATH попадает в тихий fallback (docker/vault, пустой) вместо реального хранилища, без какой-либо видимой ошибки. См. комментарий Uso: в верхней части docker/docker-compose.yml для полной информации (найдено в ревью Task 17).
3. Регистрация первой passkey (bootstrap)
В логах auth-service найдите:
[bootstrap] token de registro (10 min): <token>Откройте <PUBLIC_BASE_URL>/register?t=<token> в браузере устройства с passkey (телефон или совместимый менеджер паролей) и завершите регистрацию. Токен истекает через 10 минут; если не уложились в срок, перезапустите auth-service (docker compose restart auth-service), чтобы сгенерировать новый — это также обнуляет незавершённые сессии/challenges (SESSION_PURGE_ON_START=true по умолчанию).
Зарегистрируйте как минимум две passkey (например, телефон + менеджер паролей), пока токен bootstrap ещё действителен — это митигация данного проекта для случая «я потерял устройство»: кода восстановления нет (осознанное решение; см. спецификацию дизайна, раздел открытых решений).
4. Подключение через Custom Connector
В claude.ai -> Settings -> Connectors -> Add custom connector вставьте <PUBLIC_BASE_URL>/mcp. Оставьте поля OAuth Client пустыми (динамическая регистрация). После входа с помощью учётной записи Google, присутствующей в ALLOWED_EMAILS, полный сценарий проверки (unlock, чтение, запись с подтверждением и тест с двумя разговорами) находится в tests/integration/test_e2e_manual.md.
Известные ограничения
A8 — Человек B, открывающий тот же разговор в пределах 15-минутного окна, наследует handle. Это реальная дыра, уже задокументированная и принятая по дизайну, в модели handle: handle сессии (L1) не привязан к личности того, кто читает разговор в данный момент, а только к разговору, в котором он родился. Если учётная запись Claude является общей и Человек B открывает тот же разговор, который разблокировал Человек A — не новый разговор — в течение 15 минут абсолютного TTL (или 5 мин idle), B наследует возможность чтения (L1), полученную A. Смягчено коротким TTL, idle timeout и дополнительной привязкой к
Mcp-Session-Id, когда клиент предоставляет его стабильно, — но не устранено. Запись (L2) остаётся недостижимой для B в любом случае, поскольку требует свежей подписи passkey на каждый вызов. См. раздел A8 спецификации дизайна (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) для полного анализа угроз. Это не баг, который нужно молча исправить — это известное ограничение модели handle, общего для разговора, и Шаг 7 сценария вtests/integration/test_e2e_manual.mdсуществует именно для того, чтобы доказать, что отдельный случай (новый разговор) корректно заблокирован.Rate limiting не подключён ни к одному пути запроса. Модуль
src/authsvc/ratelimit.py(скользящее окно в памяти, классJanela) существует и имеет собственные тесты, но ни один маршрутauth-serviceилиgatewayне инстанцирует и не вызывает его — он не «подключён». На практике это означает, что митигация «brute force handle» и «систематического сканирования хранилища», описанная в разделе 20 (Тесты безопасности) и в разделе 14 (Защита от prompt injection, пункт 4) спецификации дизайна, всё ещё не существует в продакшене, хотя базовый код готов. Это реальный пробел, не покрытый ни одним другим контролем этого проекта —policies/policy.yamlсодержит секциюrate_limitsс примерными значениями (level_1: { calls: 60, window_s: 300 }), но ничто в текущемgateway_main.pyилиsrc/stepup/middleware.pyне читает эти значения, чтобы фактически ограничивать вызовы. Прежде чем выставлять этот шлюз на использование с реальным объёмом (а не только одним доверенным пользователем), подключениеratelimit.Janelaк пути L1 (и, в идеале, также к попыткам challenge/подтверждения вauth-service) следует рассматривать как приоритет, а не как полировку.Остальные структурные ограничения (без супервизии процессов, общий секрет
BACKEND_BEARER_TOKENбез разграничения по вызывающему, для публичной экспозиции требуется собственный туннель) — те же, что и уmcp-oauth-gateway, от которого этот проект наследует уровень OAuth/allowlist — см. README того проекта для подробностей.
Тесты
# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -vПокрывают: политику авторизации (src/stepup/policy.py), middleware для step-up (уровни, AUTH_REQUIRED/CONFIRMATION_REQUIRED), auth-service (WebAuthn, сессии, challenges, подтверждения, журнал аудита, HMAC-дайджест) и разрешение конфигурации docker-compose.yml (включая два режима ошибки при отсутствующем --env-file .env).
Сквозной сценарий против реального MCP-клиента и физической passkey не входит в этот набор — см. tests/integration/test_e2e_manual.md.
This server cannot be deployed
Maintenance
Related MCP Connectors
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
- JustOnceOAuthai.justonce
Persistent memory for AI assistants — one shared, OAuth-secured vault for every MCP client.
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA deny-by-default MCP server for Obsidian vaults where operators declare exact capabilities (list, read, create, etc.) scoped by path globs; everything not permitted is impossible by construction as disallowed tools are never registered.MIT
- FlicenseDqualityDmaintenanceEnables Claude Desktop to securely search and retrieve knowledge from an Obsidian vault through a stateless MCP interface, with progressive disclosure and gated write capabilities.13-
- AlicenseAqualityCmaintenanceSecure MCP server that bridges AI clients like Claude Desktop to Obsidian vaults, enabling read/write operations with OWASP Top 10 security controls and audit logging.948 npmMIT
- FlicenseNot gradedqualityBmaintenanceRemote MCP server for Obsidian vault access, giving Claude read/search/archive access to markdown notes via OAuth 2.1 + PKCE auth.2-