diff --git a/.env.example b/.env.example index 1e90b35..755d1d3 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,15 @@ PANEL_DOMAIN=panel.example.com SUB_DOMAIN=sub.example.com SITE_DOMAIN=example.com +# Optional: move the admin login off the well-known /admin path (e.g. to a +# random string) so it doesn't show up to anyone scanning for /admin, +# /login etc. Leave unset for the default. This is on top of the existing +# rate-limiting and 2FA, not instead of them. Requires `mbs restart` to +# take effect (it's a route, not a setting the running process can pick up +# live) — write it down somewhere before you restart, there's no UI for +# this on purpose, only .env + SSH can get you back in if you forget it. +ADMIN_PATH=admin + # Reality identity for the local node (this same box). Generate with: # /usr/local/bin/xray x25519 # XRAY_PUBLIC_KEY is the "Password (PublicKey)" line; keep the matching diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d2446e..ef440a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -352,3 +352,35 @@ jobs: print("backup/restore correctly round-trips branding, live settings and held_at together OK") PYEOF + + - name: Smoke test custom ADMIN_PATH actually moves the login page, not just adds a copy + env: + BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + BOT_USERNAME: "x" + ADMIN_IDS: "1" + ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" + PANEL_DOMAIN: "panel.test" + SUB_DOMAIN: "sub.test" + SITE_DOMAIN: "test" + XRAY_PUBLIC_KEY: "x" + XRAY_SHORT_ID_TCP: "x" + XRAY_SHORT_ID_GRPC: "x" + XRAY_SHORT_ID_XHTTP: "x" + ADMIN_PATH: "xyz123secret" + run: | + python - << 'PYEOF' + import api + + paths = {r.path for r in api.app.routes} + assert "/xyz123secret" in paths, "custom ADMIN_PATH must be registered as a route" + assert "/admin" not in paths, "the default /admin page route must be GONE once a custom path is set, not just supplemented" + assert "/admin/api/login" in paths, "the API namespace must stay fixed regardless of ADMIN_PATH" + + fake_request = type("FakeRequest", (), {"headers": {"host": "panel.test"}})() + root_response = api.root(fake_request) + assert isinstance(root_response, str), \ + f"root() on PANEL_DOMAIN must return the rendered site page (a string), not admin.html, once ADMIN_PATH is customized — got {type(root_response)}" + assert "admin.html" not in root_response + + print("custom ADMIN_PATH: old /admin route gone, new path registered, root() no longer leaks the panel OK") + PYEOF diff --git a/README.md b/README.md index 8c04012..5d4bc40 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - **Проверка конфига Xray перед рестартом** — `xray run -test` плюс проверка что серты реально читаемы юзером, под которым крутится Xray, до того как что-то применится и уронит сервис. - **Drag-n-drop ноды** — порядок нод в списке настраивается мышкой, как у Remnawave. - **Rate-limit на вход** — по IP, отдельно на пароль и на 2FA-код. +- **Свой путь входа** — страницу логина можно увести с дефолтного `/admin` на любой другой (`ADMIN_PATH` в `.env`), доп. слой поверх rate-limit и 2FA — у Remnawave это в списке заявленных мер безопасности, у Marzban нет вообще. ## Архитектура diff --git a/admin.html b/admin.html index a426e69..abc4080 100644 --- a/admin.html +++ b/admin.html @@ -600,6 +600,7 @@
Пароль админ-панели меняется командой mbs pass на сервере (без аргумента — сгенерит случайный). Панель физически откажется стартовать, если в .env стоит "change-me"/"admin"/что-то короче 8 символов — так что пропустить это не выйдет по-тихому.
Сессия логина живёт в httpOnly-куке, опционально поверх пароля — 2FA (TOTP). На /admin/api/login и /admin/api/login/totp висит rate-limit (10 попыток за 15 минут на пароль, 10 за 5 минут на код — с одного IP). SSH-доступ на сервер — сам по себе, панель на него не влияет; отдельно стоит подумать про отключение root-логина по паролю в пользу ключей, если этого ещё не сделано.
Логинов может быть несколько (Настройки → Админы) — у каждого свой пароль и своя 2FA, нельзя удалить последнего оставшегося админа или себя самого, пока залогинен под этим аккаунтом.
+Страницу входа можно увести с дефолтного /admin на свой путь — переменная ADMIN_PATH в .env на сервере (не через UI — это единственная настройка, которая намеренно не в панели, чтобы нельзя было опечататься и остаться без доступа без SSH). Требует mbs restart. Это доп. слой поверх rate-limit и 2FA, не замена — сам /admin/api/* не двигается, он и так защищён логином.