Skip to main content
Glama
joaorura

mcp-stepup-gateway

by joaorura

mcp-stepup-gateway

Um gateway MCP que exige passkey (WebAuthn) sob demanda -- "step-up auth" -- antes de permitir que um cliente remoto (Claude.ai, via Custom Connector) leia ou escreva num vault Obsidian protegido pelo enquire-mcp. Login Google e allowlist (como no mcp-oauth-gateway) decidem quem pode conectar; este projeto decide, tool a tool, o que essa pessoa pode fazer sem reprovar identidade de novo, e o que exige um toque fresco na passkey.

Nasceu de um caso concreto: o mcp-oauth-gateway/enquire-mcp-gateway ja resolvem "autenticar quem conecta" (OAuth + allowlist). O que faltava era uma segunda camada: mesmo dentro da allowlist, nem toda tool call deveria ser igualmente livre. Ler uma nota e barato; apagar ou reescrever conteudo do vault via uma LLM que pode estar sob prompt injection não é. Este gateway adiciona essa distincao sem tocar no enquire-mcp em si.

Por que isso existe

Um cliente MCP remoto autenticado por OAuth ainda e, do ponto de vista do vault, "uma LLM com acesso total". Isso e um problema em dois eixos:

  1. A LLM pode ser manipulada. Conteudo malicioso numa nota, ou numa resposta de ferramenta, pode tentar instruir o agente a apagar ou sobrescrever coisas -- prompt injection nao e hipotetico.

  2. "Autenticado uma vez" nao deveria significar "autorizado para sempre". Uma sessao OAuth de longa duracao nao deveria dar a mesma LLM permissao irrestrita de escrita indefinidamente, sem nenhuma prova fresca de presenca humana.

A solucao aqui e um modelo de niveis de risco por tool, com um handle de capacidade de vida curta (15 min) que autoriza leitura, e uma confirmacao com passkey por chamada que autoriza qualquer escrita ou delecao -- renderizada a partir dos argumentos reais que o servidor recebeu, nunca de texto que a LLM controla.

Related MCP server: Obsidian MCP Wrapper

Arquitetura

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)         │
└───────────────────────────────────────────────────────────┘

O gateway nunca guarda credencial nenhuma -- so fala com o auth-service (rotas internas, autenticadas por GATEWAY_KEY) para perguntar "este handle autoriza esta tool?" ou "esta confirmacao aprovou exatamente estes argumentos?". O humano nunca digita nem cola nada no chat: toda a ceremonia de passkey acontece no navegador, numa URL que o auth-service serve.

Niveis de risco

Nivel

O que exige

Exemplo

L0

Nada -- sempre liberado

vault_auth_unlock, vault_auth_check, vault_auth_status

L1

Handle de sessao valido (TTL absoluto 15 min, idle 5 min)

obsidian_search, obsidian_read_note, obsidian_list_notes

L2

Confirmacao com passkey por chamada, ligada aos argumentos exatos via args_digest (HMAC-SHA256)

obsidian_create_note, obsidian_append_to_note, obsidian_archive_note

policies/policy.yaml mapeia cada tool do backend para um nivel. Deny-by-default: qualquer tool nao mapeada explicitamente cai no nivel mais restritivo (default_level: 2) -- se o enquire-mcp ganhar uma tool nova numa atualizacao (o backend roda npx -y, entao pode mudar de versao a qualquer subida), ela chega protegida, nao aberta. Ver os comentarios no proprio policies/policy.yaml para a proveniencia dos nomes de tool usados e o que ainda precisa ser verificado ao vivo antes de producao.

Setup

Requer Docker e Docker Compose. Os tres servicos (gateway, auth-service, backend) sobem juntos.

1. Variaveis de ambiente

cp .env.example .env    # Windows: Copy-Item .env.example .env

Preencha, na raiz do repo:

  • Google OAuth (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, PUBLIC_BASE_URL, ALLOWED_EMAILS) -- mesmo padrao do mcp-oauth-gateway; veja o README daquele projeto para o passo a passo de criar o OAuth Client no Google Cloud Console.

  • WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_RP_NAME, PUBLIC_ORIGIN, GATEWAY_KEY, DIGEST_KEY) -- ver a advertencia abaixo antes de definir WEBAUTHN_RP_ID. Gere GATEWAY_KEY e DIGEST_KEY com openssl rand -hex 32.

  • Backend (BACKEND_BEARER_TOKEN, OBSIDIAN_VAULT_PATH) -- o token compartilhado entre gateway e backend, e o caminho no host do vault Obsidian a proteger.

WEBAUTHN_RP_ID e PERMANENTE. E o dominio (sem porta, sem protocolo) que fica embutido na propria assinatura WebAuthn de cada passkey registrada. Mudar esse valor depois do primeiro registro invalida TODAS as passkeys -- todo mundo precisa registrar de novo, com um novo bootstrap. Decida o dominio definitivo (o mesmo host que PUBLIC_BASE_URL, sem https://) antes de registrar a primeira passkey, nao depois. O auth-service se recusa a subir sem esta variavel definida (src/authsvc/config.py) -- de proposito: um default silencioso aqui seria pior que falhar no boot.

2. Subir o stack

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 nao e opcional -- o Docker Compose resolve ${VAR} do compose relativo ao diretorio do proprio arquivo (docker/), nao da raiz do repo. Rodar sem essa flag faz OBSIDIAN_VAULT_PATH cair num fallback silencioso (docker/vault, vazio) em vez do vault real, sem nenhum erro visivel. Ver o comentario Uso: no topo de docker/docker-compose.yml para o detalhe completo (achado do review da Task 17).

3. Registrar a primeira passkey (bootstrap)

Nos logs do auth-service, procure:

[bootstrap] token de registro (10 min): <token>

Abra <PUBLIC_BASE_URL>/register?t=<token> no navegador de um dispositivo com passkey (celular, ou um gerenciador de senhas compativel) e complete o registro. O token expira em 10 minutos; se perder o prazo, reinicie o auth-service (docker compose restart auth-service) para gerar outro -- isso tambem zera sessoes/challenges pendentes (SESSION_PURGE_ON_START=true por padrao).

Registre pelo menos duas passkeys (celular + gerenciador de senhas, por exemplo) enquanto o token de bootstrap ainda vale -- e a mitigacao deste projeto para "perdi o dispositivo": nao ha codigo de recuperacao (decisao deliberada; ver a spec de design, secao de decisoes abertas).

4. Conectar como Custom Connector

Em claude.ai -> Settings -> Connectors -> Add custom connector, cole <PUBLIC_BASE_URL>/mcp. Deixe os campos de OAuth Client vazios (registro dinamico). Depois de logar com uma conta Google presente em ALLOWED_EMAILS, o roteiro completo de verificacao (unlock, leitura, escrita com confirmacao, e o teste de duas conversas) esta em tests/integration/test_e2e_manual.md.

Limitações conhecidas

  • A8 -- Pessoa B abrindo a mesma conversa dentro da janela de 15 minutos herda o handle. Este e o furo real, ja documentado e aceito por design, do modelo de handle: o handle de sessao (L1) nao esta ligado a identidade de quem esta lendo a conversa naquele momento, so a conversa onde ele nasceu. Se a conta Claude e compartilhada e a Pessoa B abre a mesma conversa que a Pessoa A destravou -- nao uma conversa nova -- dentro dos 15 minutos de TTL absoluto (ou 5 min de idle), B herda a capacidade de leitura (L1) que A obteve. Mitigado por TTL curto, idle timeout, e binding adicional ao Mcp-Session-Id quando o cliente o fornece de forma estavel -- mas nao eliminado. Escrita (L2) permanece inatingivel para B em qualquer caso, porque exige uma assinatura de passkey fresca por chamada. Ver a secao A8 da spec de design (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) para a analise completa de ameaca. Isto nao e um bug a ser corrigido silenciosamente -- e uma limitacao conhecida do modelo de handle compartilhado por conversa, e o Passo 7 do roteiro em tests/integration/test_e2e_manual.md existe justamente para provar que o caso distinto (conversa nova) esta corretamente bloqueado.

  • Rate limiting nao esta conectado a nenhum caminho de requisicao. O modulo src/authsvc/ratelimit.py (janela deslizante em memoria, classe Janela) existe e tem testes proprios, mas nenhuma rota do auth-service nem do gateway o instancia ou chama -- ele nao esta "wireado". Na pratica, isso significa que a mitigacao de "brute force de handle" e "varredura sistematica do vault" descrita na secao 20 (Testes de seguranca) e na secao 14 (Protecao contra prompt injection, item 4) da spec de design ainda nao existe em producao, apesar do codigo base estar pronto. Isto e uma lacuna real, nao coberta por nenhum outro controle deste projeto -- policies/policy.yaml tem uma secao rate_limits com valores de exemplo (level_1: { calls: 60, window_s: 300 }), mas nada no gateway_main.py ou no src/stepup/middleware.py atual le esses valores para de fato limitar chamadas. Antes de expor este gateway a um uso com volume real (nao so um unico usuario confiavel), conectar ratelimit.Janela ao caminho de L1 (e, idealmente, tambem a tentativas de challenge/confirmacao no auth-service) deveria ser tratado como prioridade, nao como polimento.

  • Demais limitacoes estruturais (sem supervisao de processo, segredo BACKEND_BEARER_TOKEN compartilhado sem escopo por chamador, exposicao publica exige seu proprio tunel) sao as mesmas do mcp-oauth-gateway, do qual este projeto herda a camada de OAuth/allowlist -- ver o README daquele projeto para os detalhes.

Testes

# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -v

Cobrem: a politica de autorizacao (src/stepup/policy.py), o middleware de step-up (niveis, AUTH_REQUIRED/CONFIRMATION_REQUIRED), o auth-service (WebAuthn, sessoes, challenges, confirmacoes, audit log, digest HMAC), e a resolucao de configuracao do docker-compose.yml (incluindo os dois modos de erro do --env-file .env ausente).

O roteiro ponta a ponta contra um cliente MCP real e uma passkey fisica nao esta nesta suite -- ver 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
    -