Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

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 с полным доступом». Это проблема в двух аспектах:

  1. LLM можно манипулировать. Вредоносное содержимое в заметке или в ответе инструмента может попытаться указать агенту удалить или перезаписать что-то — prompt injection не гипотетичен.

  2. «Аутентифицированный один раз» не должно означать «авторизованный навсегда». Длительная 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

Ничего — всегда разрешено

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

Действительный handle сессии (абсолютный TTL 15 мин, idle 5 мин)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

Подтверждение passkey на каждый вызов, привязанное к точным аргументам через args_digest (HMAC-SHA256)

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A 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
  • F
    license
    D
    quality
    D
    maintenance
    Enables 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
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Remote MCP server for Obsidian vault access, giving Claude read/search/archive access to markdown notes via OAuth 2.1 + PKCE auth.
    2
    -