From bc82bbab1975515cfc006cbf6a5dfee4395a8e81 Mon Sep 17 00:00:00 2001 From: "nikita.hohlov" Date: Sat, 5 Sep 2026 07:25:31 -0300 Subject: [PATCH] feat: trust X-Remote-User only from the proxy (HADO_TRUSTED_PROXIES); open tailnet inbound for the agent Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw --- core/.env.example | 2 ++ core/app/Http/Middleware/RemoteUser.php | 10 +++++++-- core/config/hado.php | 2 ++ core/tests/Feature/RemoteUserTrustTest.php | 25 ++++++++++++++++++++++ docker-compose.prod.yml | 5 +++-- 5 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 core/tests/Feature/RemoteUserTrustTest.php diff --git a/core/.env.example b/core/.env.example index 9f0c5a9..3919b4a 100644 --- a/core/.env.example +++ b/core/.env.example @@ -43,6 +43,8 @@ CACHE_STORE=database HADO_DEFAULT_TZ=America/Montevideo # Только dev (APP_ENV=local): логин вместо X-Remote-User, чтобы открыть инбокс без хаба HADO_DEV_USER= +# Откуда верим X-Remote-User (TCP-сосед): loopback и докер-мосты, т.е. Caddy. С тейлнета заголовок не принимается. +HADO_TRUSTED_PROXIES=127.0.0.0/8,172.16.0.0/12 HADO_PRESENCE_TIMEOUT=2 HADO_FALLBACK_DAY=30 HADO_FALLBACK_HOUR=10 diff --git a/core/app/Http/Middleware/RemoteUser.php b/core/app/Http/Middleware/RemoteUser.php index e2d9e0e..971dbcc 100644 --- a/core/app/Http/Middleware/RemoteUser.php +++ b/core/app/Http/Middleware/RemoteUser.php @@ -5,11 +5,14 @@ namespace App\Http\Middleware; use App\Ingest\EventUpserter; use Closure; use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpFoundation\Response; /** - * Доверяет X-Remote-User от Caddy forward_auth (сеть изолирована, как в sekai). - * На публичных маршрутах (/api, /a, /hooks) этот middleware не стоит. + * Доверяет X-Remote-User от Caddy forward_auth — и только ему: заголовок принимается, + * если TCP-сосед (REMOTE_ADDR, не X-Forwarded-For) из HADO_TRUSTED_PROXIES. Порт hado + * виден и из тейлнета (агент), оттуда подделать SSO-заголовок нельзя. + * На публичных маршрутах (/api, /a, /hooks, /mcp) этот middleware не стоит. */ class RemoteUser { @@ -18,6 +21,9 @@ class RemoteUser public function handle(Request $request, Closure $next): Response { $login = trim((string) $request->header('X-Remote-User', '')); + if ($login !== '' && ! IpUtils::checkIp((string) $request->server('REMOTE_ADDR'), config('hado.trusted_proxies'))) { + return response()->json(['message' => 'X-Remote-User принимается только от прокси'], 401); + } if ($login === '' && app()->environment('local')) { // Dev без хаба: HADO_DEV_USER подставляет пользователя. В проде не работает. $login = trim((string) config('hado.dev_user')); diff --git a/core/config/hado.php b/core/config/hado.php index 069f25c..239ecbb 100644 --- a/core/config/hado.php +++ b/core/config/hado.php @@ -4,6 +4,8 @@ return [ 'default_tz' => env('HADO_DEFAULT_TZ', 'UTC'), // Только для APP_ENV=local: логин, который подставляется вместо X-Remote-User, если заголовка нет. 'dev_user' => env('HADO_DEV_USER', ''), + // Откуда верим X-Remote-User: loopback и докер-мосты (Caddy заходит через docker-proxy с адреса шлюза). + 'trusted_proxies' => array_map('trim', explode(',', env('HADO_TRUSTED_PROXIES', '127.0.0.0/8,172.16.0.0/12'))), 'presence_timeout' => (int) env('HADO_PRESENCE_TIMEOUT', 2), 'presence_ttl' => 60, 'fallback_day_minutes' => (int) env('HADO_FALLBACK_DAY', 30), diff --git a/core/tests/Feature/RemoteUserTrustTest.php b/core/tests/Feature/RemoteUserTrustTest.php new file mode 100644 index 0000000..69896a0 --- /dev/null +++ b/core/tests/Feature/RemoteUserTrustTest.php @@ -0,0 +1,25 @@ +withServerVariables(['REMOTE_ADDR' => '172.21.0.1'])->getJson('/me', ['X-Remote-User' => 'luci'])->assertOk(); + $this->withServerVariables(['REMOTE_ADDR' => '127.0.0.1'])->getJson('/me', ['X-Remote-User' => 'luci'])->assertOk(); + } + + public function test_header_from_tailnet_or_internet_is_rejected(): void + { + $this->withServerVariables(['REMOTE_ADDR' => '100.124.15.61'])->getJson('/me', ['X-Remote-User' => 'luci']) + ->assertStatus(401)->assertJsonPath('message', 'X-Remote-User принимается только от прокси'); + $this->withServerVariables(['REMOTE_ADDR' => '8.8.8.8'])->getJson('/me', ['X-Remote-User' => 'luci'])->assertStatus(401); + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4fa5ddf..6abf8d7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -6,9 +6,10 @@ # Tailscale — сайдкар в netns приложения: в тейлнете только этот стек, хост и остальные # сервисы не затронуты. Владелец netns — app (самый стабильный), tailscale/scheduler/worker # садятся к нему: рестарт tailscale не роняет сайт. Контейнер крутит голый tailscaled, -# логин — один раз руками: `docker compose exec tailscale tailscale up --shields-up --hostname=hado --accept-dns=false` +# логин — один раз руками: `docker compose exec tailscale tailscale up --hostname=hado --accept-dns=false` # (печатает ссылку, ждёт; состояние в volume, дальше поднимается сам). -# --shields-up: нода ничего не принимает из тейлнета, только ходит наружу (в Home Assistant). +# Из тейлнета hado доступен как http://:8080 (агент Мия → /mcp); X-Remote-User с тейлнета +# ядро не принимает — только от Caddy (HADO_TRUSTED_PROXIES), так что подделать SSO оттуда нельзя. services: app: build: ./core