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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
@@ -43,6 +43,8 @@ CACHE_STORE=database
|
|||||||
HADO_DEFAULT_TZ=America/Montevideo
|
HADO_DEFAULT_TZ=America/Montevideo
|
||||||
# Только dev (APP_ENV=local): логин вместо X-Remote-User, чтобы открыть инбокс без хаба
|
# Только dev (APP_ENV=local): логин вместо X-Remote-User, чтобы открыть инбокс без хаба
|
||||||
HADO_DEV_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_PRESENCE_TIMEOUT=2
|
||||||
HADO_FALLBACK_DAY=30
|
HADO_FALLBACK_DAY=30
|
||||||
HADO_FALLBACK_HOUR=10
|
HADO_FALLBACK_HOUR=10
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ namespace App\Http\Middleware;
|
|||||||
use App\Ingest\EventUpserter;
|
use App\Ingest\EventUpserter;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\IpUtils;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Доверяет X-Remote-User от Caddy forward_auth (сеть изолирована, как в sekai).
|
* Доверяет X-Remote-User от Caddy forward_auth — и только ему: заголовок принимается,
|
||||||
* На публичных маршрутах (/api, /a, /hooks) этот middleware не стоит.
|
* если TCP-сосед (REMOTE_ADDR, не X-Forwarded-For) из HADO_TRUSTED_PROXIES. Порт hado
|
||||||
|
* виден и из тейлнета (агент), оттуда подделать SSO-заголовок нельзя.
|
||||||
|
* На публичных маршрутах (/api, /a, /hooks, /mcp) этот middleware не стоит.
|
||||||
*/
|
*/
|
||||||
class RemoteUser
|
class RemoteUser
|
||||||
{
|
{
|
||||||
@@ -18,6 +21,9 @@ class RemoteUser
|
|||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
$login = trim((string) $request->header('X-Remote-User', ''));
|
$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')) {
|
if ($login === '' && app()->environment('local')) {
|
||||||
// Dev без хаба: HADO_DEV_USER подставляет пользователя. В проде не работает.
|
// Dev без хаба: HADO_DEV_USER подставляет пользователя. В проде не работает.
|
||||||
$login = trim((string) config('hado.dev_user'));
|
$login = trim((string) config('hado.dev_user'));
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ return [
|
|||||||
'default_tz' => env('HADO_DEFAULT_TZ', 'UTC'),
|
'default_tz' => env('HADO_DEFAULT_TZ', 'UTC'),
|
||||||
// Только для APP_ENV=local: логин, который подставляется вместо X-Remote-User, если заголовка нет.
|
// Только для APP_ENV=local: логин, который подставляется вместо X-Remote-User, если заголовка нет.
|
||||||
'dev_user' => env('HADO_DEV_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_timeout' => (int) env('HADO_PRESENCE_TIMEOUT', 2),
|
||||||
'presence_ttl' => 60,
|
'presence_ttl' => 60,
|
||||||
'fallback_day_minutes' => (int) env('HADO_FALLBACK_DAY', 30),
|
'fallback_day_minutes' => (int) env('HADO_FALLBACK_DAY', 30),
|
||||||
|
|||||||
25
core/tests/Feature/RemoteUserTrustTest.php
Normal file
25
core/tests/Feature/RemoteUserTrustTest.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/** X-Remote-User верим только соседу по докер-сети (Caddy); с тейлнета или снаружи заголовок — не пропуск. */
|
||||||
|
class RemoteUserTrustTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_header_is_trusted_from_docker_bridge_and_loopback(): void
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
# Tailscale — сайдкар в netns приложения: в тейлнете только этот стек, хост и остальные
|
# Tailscale — сайдкар в netns приложения: в тейлнете только этот стек, хост и остальные
|
||||||
# сервисы не затронуты. Владелец netns — app (самый стабильный), tailscale/scheduler/worker
|
# сервисы не затронуты. Владелец netns — app (самый стабильный), tailscale/scheduler/worker
|
||||||
# садятся к нему: рестарт tailscale не роняет сайт. Контейнер крутит голый tailscaled,
|
# садятся к нему: рестарт 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, дальше поднимается сам).
|
# (печатает ссылку, ждёт; состояние в volume, дальше поднимается сам).
|
||||||
# --shields-up: нода ничего не принимает из тейлнета, только ходит наружу (в Home Assistant).
|
# Из тейлнета hado доступен как http://<ts-ip>:8080 (агент Мия → /mcp); X-Remote-User с тейлнета
|
||||||
|
# ядро не принимает — только от Caddy (HADO_TRUSTED_PROXIES), так что подделать SSO оттуда нельзя.
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: ./core
|
build: ./core
|
||||||
|
|||||||
Reference in New Issue
Block a user