Repository navigation
feat: super password and 24-hour super session for dangerous actions and API key writes - #31
Merged
Merged
Conversation
Что: - docker.listContainerFiles, readContainerFile, writeContainerFile и deleteContainerFile теперь проходят assertDockerServerAccess (только owner/admin и доступный сервер) и resolveAuthorizedContainerId (локальный контейнер должен принадлежать сервису, к которому есть доступ). - writeContainerFile требует docker.write, deleteContainerFile требует docker.delete вместо docker.read. - docker.getEvents использует ту же проверку owner/admin и доступа к серверу. - dockerVolume.listVolumeFiles, readVolumeFile, writeVolumeFile, deleteVolumeFile и removeVolume используют assertDockerServerAccess; запись требует docker.write, удаление docker.delete. Зачем: - Раньше эти процедуры проверяли только docker.read и принадлежность сервера организации, поэтому пользователь с docker.read мог читать, менять и удалять файлы в любом контейнере или volume и удалять volumes. Риски: - Owner/admin больше не могут открыть файлы локальных контейнеров, которые не привязаны к сервису Dokploy (так же, как уже работают restart/upload). - Пользователи с кастомными ролями, у которых есть docker.read, больше не видят файлы контейнеров и volumes и Docker events. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/docker/docker-router-security.test.ts __test__/docker/docker-volume-router-security.test.ts - все тесты прошли; новые тесты падают на старом коде (исходники временно возвращены к origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок. - Ограничения: UI проверен только чтением кода (файловый менеджер есть только на странице Docker, где список контейнеров уже требует owner/admin); на реальном Docker не запускалось. What: - docker.listContainerFiles, readContainerFile, writeContainerFile and deleteContainerFile now go through assertDockerServerAccess (owner/admin plus an accessible server) and resolveAuthorizedContainerId (a local container must belong to a service the caller may access). - writeContainerFile requires docker.write and deleteContainerFile requires docker.delete instead of docker.read. - docker.getEvents uses the same owner/admin and server access check. - dockerVolume.listVolumeFiles, readVolumeFile, writeVolumeFile, deleteVolumeFile and removeVolume use assertDockerServerAccess; writes require docker.write, deletes docker.delete. Why: - These procedures only checked docker.read and that the server belongs to the organization, so a docker.read holder could read, change and delete files in any container or volume and remove volumes. Risks: - Owners/admins can no longer open files of local containers that are not bound to a Dokploy service (the same rule restart/upload already follow). - Custom roles with docker.read no longer see container/volume files or Docker events. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/docker/docker-router-security.test.ts __test__/docker/docker-volume-router-security.test.ts - all passed; the new tests fail on the old code (sources temporarily reset to origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check on changed files - no errors. - Limitations: UI checked by reading code only (the file explorer lives on the Docker page, whose container list already requires owner/admin); not run against a real Docker host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- readLogs в роутерах application, compose, postgres, mysql, mariadb, mongo, redis и libsql теперь вызывает checkServicePermissionAndAccess с { service: ["read"], logs: ["read"] } вместо checkServiceAccess(..., "read").
Зачем:
- Право logs.read есть в модели прав, но эти процедуры его не проверяли: кастомная роль без logs.read все равно могла читать логи контейнеров сервиса.
Риски:
- Кастомные роли без logs.read теряют доступ к логам сервисов. Статические роли owner, admin и member имеют logs.read, их поведение не меняется.
Проверки:
- Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/logs/read-logs-permission.test.ts - 8 тестов прошли; на старом коде все 8 падают (исходники временно возвращены к origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок.
- Ограничения: логи через WebSocket (server/wss) не менялись.
What:
- readLogs in the application, compose, postgres, mysql, mariadb, mongo, redis and libsql routers now calls checkServicePermissionAndAccess with { service: ["read"], logs: ["read"] } instead of checkServiceAccess(..., "read").
Why:
- logs.read exists in the permission model but these procedures never checked it, so a custom role without logs.read could still read a service's container logs.
Risks:
- Custom roles without logs.read lose access to service logs. The static owner, admin and member roles all grant logs.read, so their behavior is unchanged.
Checks:
- Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/logs/read-logs-permission.test.ts - 8 tests passed; all 8 fail on the old code (sources temporarily reset to origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check on changed files - no errors.
- Limitations: WebSocket log streaming (server/wss) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что: - resolveTraefikConfigPath отклоняет acme.json, файлы *.key и все внутри dynamic/certificates, поэтому settings.readTraefikFile и settings.updateTraefikFile не могут читать или перезаписывать их ни локально, ни на удаленном сервере. - settings.readDirectories скрывает эти файлы и папку certificates из дерева. - readDirectories, readTraefikFile и updateTraefikFile без serverId требуют доступ к локальному хосту (owner/admin, assertLocalHostAccess), как другие процедуры локального хоста. Зачем: - Пользователь с traefikFiles.read мог прочитать acme.json и приватные ключи сертификатов, а без serverId не было проверки роли для локального хоста. Риски: - Эти файлы больше нельзя открыть или изменить в редакторе Traefik даже owner/admin; сертификаты по-прежнему управляются через раздел сертификатов. - Кастомные роли с traefikFiles.* теряют доступ к файлам Traefik локального хоста; удаленные серверы работают как раньше. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/traefik/traefik-file-boundary.test.ts __test__/settings/docker-server-boundary.test.ts - все тесты прошли; новые тесты падают на старом коде (исходники временно возвращены к origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок. - Ограничения: симлинки внутри папки Traefik не разрешаются (проверка идет по пути); на реальном сервере не проверялось. What: - resolveTraefikConfigPath rejects acme.json, *.key files and anything under dynamic/certificates, so settings.readTraefikFile and settings.updateTraefikFile can no longer read or overwrite them, locally or on a remote server. - settings.readDirectories hides these files and the certificates directory from the tree. - readDirectories, readTraefikFile and updateTraefikFile without a serverId require local host access (owner/admin, assertLocalHostAccess), like the other local host procedures. Why: - A traefikFiles.read holder could read acme.json and certificate private keys, and without a serverId there was no role check for the local host. Risks: - These files can no longer be opened or edited in the Traefik editor, even by owners/admins; certificates are still managed through the certificates section. - Custom roles with traefikFiles.* lose access to local host Traefik files; remote servers work as before. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/traefik/traefik-file-boundary.test.ts __test__/settings/docker-server-boundary.test.ts - all passed; the new tests fail on the old code (sources temporarily reset to origin/AgentHits-Dev); bun run typecheck - exit 0; bunx biome check on changed files - no errors. - Limitations: symlinks inside the Traefik directory are not resolved (the check is path based); not checked on a real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into feat/super-password
Что: - Новые таблицы super_password (bcrypt-хеш, подсказка, счетчик ошибок, блокировка), super_session (одна сессия на пользователя, срок 24 часа) и super_password_token (хеши одноразовых ссылок reset и lock). Одна миграция 0204, сгенерирована drizzle-kit. - Сервис packages/server/src/services/super-password.ts: установка, смена, отключение, проверка пароля с блокировкой на 15 минут после 5 ошибок, открытие, продление (только пока сессия открыта) и закрытие супер-сессии, одноразовые токены с хешем в БД, очистка просроченных токенов, правила доступа read/write/dangerous. - Супер-пароль хешируется bcrypt, как пароли входа; совпадение с паролем входа (credential account) отклоняется; подсказка до 200 символов и не может содержать пароль. - Письма (разблокировка с временем, IP, user agent и ссылкой "This wasn't me", блокировка после ошибок, ссылка сброса) уходят через email-канал уведомлений организации (SMTP или Resend) на адрес аккаунта. Зачем: - Основа для второго секрета: утекший API-ключ или сессия браузера не должны давать запись и доступ к секретам без супер-пароля. Риски: - Пока пользователь не задал супер-пароль, правила возвращают текущее поведение. - Письма зависят от настроенного email-уведомления; без него функция работает, но писем нет. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-password-service.test.ts - 18 тестов прошли; на origin/AgentHits-Dev файл падает (сервиса и таблиц нет); bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок; полный набор тестов прошел (реальные Docker-тесты прошли на локальном OrbStack со swarm). - Ограничения: тесты сервиса используют БД в памяти, а не настоящий Postgres; отправка писем проверена моками. What: - New tables super_password (bcrypt hash, hint, failed attempt counter, lock), super_session (one per user, 24 hours) and super_password_token (hashes of single-use reset and lock links). One migration 0204, generated with drizzle-kit. - Service packages/server/src/services/super-password.ts: set, change, turn off, verify with a 15-minute lock after 5 wrong attempts, open, extend (only while open) and close the super session, single-use tokens stored as hashes, expired token cleanup, read/write/dangerous access rules. - The super password is hashed with bcrypt like login passwords; a value equal to the credential account password is rejected; the hint is at most 200 characters and must not contain the password. - Emails (unlock with time, IP, user agent and a "This wasn't me" link, lock after wrong attempts, reset link) go through the organization's email notification channel (SMTP or Resend) to the account email. Why: - Groundwork for a second secret: a leaked API key or browser session must not give writes or secret access without the super password. Risks: - Until a user sets a super password, the rules keep today's behaviour. - Emails depend on a configured email notification; without one the feature works but sends nothing. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-password-service.test.ts - 18 tests passed; the file fails on origin/AgentHits-Dev (no service or tables); bun run typecheck - exit 0; bunx biome check on changed files - no errors; the full test suite passed (real Docker tests passed on local OrbStack with swarm). - Limitations: service tests use an in-memory database, not a real Postgres; email sending is checked with mocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ession Что: - validateRequest помечает сессии из x-api-key как authMethod "api-key", cookie-сессии как "session". - Все процедуры tRPC (public, protected, admin, owner, enterprise, cli) проходят через общий middleware. Для пользователя с супер-паролем: мутации и подписки через API-ключ без открытой супер-сессии получают FORBIDDEN "API key write access is locked. Open it in Profile → Super password."; опасные процедуры требуют браузерную сессию и открытую супер-сессию, API-ключ их не выполняет никогда. - Центральный реестр server/api/utils/procedure-access.ts классифицирует все 650 процедур app router как read, write или dangerous (для user.update, schedule.create/update/runManually, backup.create/update — по входу: смена email/пароля, dokploy-server расписания, бэкап web-server). Чтение файлов контейнеров и volumes через API-ключ считается записью. - Роутер superPassword: status (работает и через API-ключ, без хеша), set, unlock, extend, close, change, disable, requestReset, resetWithToken и публичный lockWithToken. set, unlock, extend, change, disable и сброс доступны только из браузерной сессии; set только owner/admin. - REST-эндпоинт application env upsert применяет то же правило записи для API-ключей. - user.update отклоняет новый пароль входа, совпадающий с супер-паролем. - errorFormatter добавляет data.superSession с причиной отказа, чтобы UI мог предложить разблокировку. Зачем: - Утекший API-ключ без открытой супер-сессии остается только на чтение (секреты в ответах уже скрыты), а секреты, доступы, настройки хоста и удаления требуют супер-пароль даже при украденной сессии браузера. Риски: - Пользователи без супер-пароля работают как раньше, AI-агент владельца не ломается до установки пароля. - После установки пароля API-ключи владельца пишут только при открытой супер-сессии (24 часа, продление в профиле), а опасные действия через API-ключ недоступны совсем. - Новые процедуры из upstream без классификации ведут себя по типу (query - чтение, mutation - запись), а тест классификации падает. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-session-guard.test.ts __test__/security/super-session-procedure-access.test.ts __test__/security/super-session-env-upsert.test.ts - 21 тест прошел; на origin/AgentHits-Dev они падают; bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок; полный набор тестов прошел. - Ограничения: OpenAPI-обработчик проверен чтением кода (он вызывает appRouter.createCaller с полным путем процедуры); живой MCP-клиент не запускался. What: - validateRequest marks sessions built from x-api-key with authMethod "api-key" and cookie sessions with "session". - Every tRPC procedure (public, protected, admin, owner, enterprise, cli) goes through one middleware. For a user with a super password: API key mutations and subscriptions without an open super session get FORBIDDEN "API key write access is locked. Open it in Profile → Super password."; dangerous procedures need a browser session and an open super session, and API keys can never run them. - A central registry server/api/utils/procedure-access.ts classifies all 650 app router procedures as read, write or dangerous (user.update, schedule.create/update/runManually and backup.create/update by input: email or password change, dokploy-server schedules, web server backups). Container and volume file reads count as writes for API keys. - superPassword router: status (also through API keys, never the hash), set, unlock, extend, close, change, disable, requestReset, resetWithToken and a public lockWithToken. set, unlock, extend, change, disable and reset only work from a browser session; set only for owner/admin. - The application env upsert REST endpoint applies the same API key write rule. - user.update rejects a new login password equal to the super password. - errorFormatter adds data.superSession with the denial reason so the UI can offer to unlock. Why: - A leaked API key without an open super session stays read-only (secrets in responses are already redacted), and secrets, access, host settings and deletions need the super password even with a stolen browser session. Risks: - Users without a super password keep today's behaviour, so the owner's AI agent keeps working until the password is set. - After it is set, the owner's API keys write only while the super session is open (24 hours, extend in Profile), and dangerous actions are never available through API keys. - Procedures added upstream without a classification fall back to their type (query read, mutation write) and the classification test fails. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-session-guard.test.ts __test__/security/super-session-procedure-access.test.ts __test__/security/super-session-env-upsert.test.ts - 21 tests passed; they fail on origin/AgentHits-Dev; bun run typecheck - exit 0; bunx biome check on changed files - no errors; the full test suite passed. - Limitations: the OpenAPI handler was checked by reading code (it calls appRouter.createCaller with the full procedure path); no live MCP client was run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что: - В better-auth hooks.before добавлена проверка супер-сессии для эндпоинтов, меняющих доступ: создание, изменение и удаление API-ключей, смена email и пароля, удаление аккаунта, привязка и отвязка соцсетей, участники, приглашения, роли и команды организации, включение и выключение 2FA, коды восстановления и TOTP URI, регистрация и удаление passkey, настройки SSO-провайдеров. Для пользователя с супер-паролем без открытой супер-сессии они отвечают 403 с кодом SUPER_SESSION_REQUIRED. - Вход, выход, получение и обновление сессии, проверка 2FA и passkey при входе, сброс пароля по ссылке и отзыв сессий не проверяются, поэтому закрытая супер-сессия никого не блокирует. - Терминалы по WebSocket (локальный хост, удаленные серверы, контейнеры) для пользователя с супер-паролем требуют cookie-сессию и открытую супер-сессию; иначе сокет получает понятное сообщение и закрывается с кодом 4003. Зачем: - Многие действия с пользователями и доступом идут напрямую в /api/auth/*, а терминалы дают shell на хосте, поэтому без этих проверок украденная сессия обходила бы супер-пароль. Риски: - Пользователи без супер-пароля работают как раньше. - Новые эндпоинты better-auth, меняющие доступ, нужно добавлять в SUPER_SESSION_AUTH_PATHS вручную. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-session-auth-hook.test.ts __test__/wss/terminal-super-session.test.ts - 9 тестов прошли (WebSocket-тест поднимает настоящий http-сервер и подключается клиентом ws); на origin/AgentHits-Dev они падают; bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок; полный набор тестов прошел. - Ограничения: хук проверен на уровне функции с замоканным getSessionFromCtx, полный запрос через better-auth handler не запускался. What: - better-auth hooks.before now checks the super session for endpoints that change access: API key create, update and delete, email and password change, account deletion, social link and unlink, organization members, invitations, roles and teams, 2FA enable and disable, backup codes and TOTP URI, passkey registration and removal, SSO provider settings. For a user with a super password and no open super session they answer 403 with code SUPER_SESSION_REQUIRED. - Sign-in, sign-out, getting and refreshing the session, 2FA and passkey verification during sign-in, password reset by link and session revocation are not checked, so a closed super session locks nobody out. - WebSocket terminals (local host, remote servers, containers) need the cookie session and an open super session for a user with a super password; otherwise the socket gets a clear message and closes with code 4003. Why: - Many user and access actions go straight to /api/auth/*, and terminals give a host shell, so without these checks a stolen session would bypass the super password. Risks: - Users without a super password keep today's behaviour. - New better-auth endpoints that change access must be added to SUPER_SESSION_AUTH_PATHS by hand. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-session-auth-hook.test.ts __test__/wss/terminal-super-session.test.ts - 9 tests passed (the WebSocket test starts a real http server and connects with a ws client); they fail on origin/AgentHits-Dev; bun run typecheck - exit 0; bunx biome check on changed files - no errors; the full test suite passed. - Limitations: the hook is tested as a function with a mocked getSessionFromCtx; a full request through the better-auth handler was not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… session Что: - Жесткие запреты из PR #26 заменены проверкой супер-сессии. acme.json, файлы *.key и все внутри dynamic/certificates читаются и записываются через редактор Traefik только owner/admin из браузерной сессии с открытой супер-сессией (локально и на удаленных серверах). Пока супер-сессия закрыта, эти файлы скрыты из дерева readDirectories и запрещены, как в #26. - resolveTraefikConfigPath, readConfigInPath и writeTraefikConfigInPath принимают allowProtected; его передают только процедуры, которые уже проверили супер-сессию. - Файловый менеджер контейнеров (list/read/write/deleteContainerFile) открывает локальные контейнеры без сервиса Dokploy (сама панель, ее postgres и redis, traefik) только owner/admin с браузерной сессией и открытой супер-сессией. Контейнеры сервисов работают как в #26; restart, stop, kill, remove и upload по-прежнему только для контейнеров сервисов. - Файловый менеджер volumes так же требует супер-сессию для локальных volumes, которые смонтированы в контейнеры без сервиса (например, volume базы панели). Зачем: - Владелец должен иметь доступ к TLS-файлам и контейнерам панели, но украденная сессия или API-ключ без супер-пароля - нет. Риски: - Без установленного супер-пароля эти файлы и контейнеры недоступны, как и после #26. - Определение системного volume идет через docker ps --filter volume; volume, который сейчас не смонтирован ни в один контейнер, считается обычным. - Проверка путей Traefik по-прежнему без разрешения симлинков. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/docker/docker-router-security.test.ts __test__/docker/docker-volume-router-security.test.ts __test__/docker/local-docker-system-access.test.ts __test__/settings/docker-server-boundary.test.ts __test__/traefik/traefik-file-boundary.test.ts - 81 тест прошел; новые тесты падают на коде #26 (жесткий запрет) и на origin/AgentHits-Dev; bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок; полный набор тестов прошел. - Ограничения: на реальном хосте с панелью не проверялось. What: - The hard denies from PR #26 become a super session check. acme.json, *.key files and anything under dynamic/certificates can be read and written through the Traefik editor only by owner/admin from a browser session with an open super session (locally and on remote servers). While the super session is closed these files stay hidden from readDirectories and denied, as in #26. - resolveTraefikConfigPath, readConfigInPath and writeTraefikConfigInPath take allowProtected; only procedures that already checked the super session pass it. - The container file manager (list/read/write/deleteContainerFile) opens local containers that no Dokploy service owns (the panel itself, its postgres and redis, traefik) only for owner/admin with a browser session and an open super session. Service containers work as in #26; restart, stop, kill, remove and upload still only accept service containers. - The volume file manager likewise needs the super session for local volumes mounted by containers without a service (for example the panel database volume). Why: - The owner must be able to reach TLS files and panel containers, but a stolen session or API key without the super password must not. Risks: - Without a super password set these files and containers stay unavailable, as after #26. - System volumes are detected with docker ps --filter volume; a volume that no container mounts right now counts as a regular one. - The Traefik path check still does not resolve symlinks. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/docker/docker-router-security.test.ts __test__/docker/docker-volume-router-security.test.ts __test__/docker/local-docker-system-access.test.ts __test__/settings/docker-server-boundary.test.ts __test__/traefik/traefik-file-boundary.test.ts - 81 tests passed; the new tests fail on the #26 code (hard deny) and on origin/AgentHits-Dev; bun run typecheck - exit 0; bunx biome check on changed files - no errors; the full test suite passed. - Limitations: not checked on a real host running the panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mands Что: - Карточка "Super password" в профиле: заметное предупреждение "Not set" и форма установки (пароль, подтверждение, подсказка); при закрытом доступе - форма разблокировки с подсказкой; при открытом - обратный отсчет "Access open: 23 h 59 min left" (обновляется, предупреждение меньше чем за час), кнопки "Extend to 24 h" и "Close access now"; смена пароля, отключение, "Forgot super password? Send reset link" или текст "Email reset unavailable: configure an email notification, or use SSH". - Глобальный диалог разблокировки: tRPC-link перехватывает ответ с data.superSession = super-session-required, просит супер-пароль и повторяет запрос; для эндпоинтов better-auth диалог открывается из fetchOptions.onError, а само действие пользователь повторяет сам. Если пароль не задан, диалог ведет в Profile → Super password. - Страница /super-password/lock (без входа): кнопка "This wasn't me: close access" закрывает доступ по одноразовой ссылке из письма; закрытие происходит по нажатию, а не при открытии страницы, чтобы почтовые сканеры ссылок не срабатывали. - Страница /dashboard/settings/super-password-reset: новый супер-пароль по одноразовой ссылке сброса, только для того же вошедшего пользователя. - SSH-команды reset-super-password (удаляет супер-пароль и закрывает сессию, по email или для владельца) и lock-super-session (закрывает сессию), собираются esbuild вместе с reset-password и reset-2fa. Зачем: - Владельцу нужен понятный способ открыть, продлить и закрыть доступ, а также аварийно сбросить или заблокировать его через почту или SSH. Риски: - Интерфейс только на английском, как остальные компоненты профиля (переводов там нет). Проверки: - Команды и результаты: bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок; полный набор тестов прошел. Приложение запущено локально (Postgres в OrbStack, dev-сервер на 127.0.0.1): в браузере проверены установка пароля (совпадение с паролем входа отклонено), диалог при создании API-ключа (неверный пароль, затем верный - ключ создан повтором), отсчет с предупреждением меньше часа, продление, закрытие, блокировка API-ключа на запись при закрытом доступе, отказ API-ключу в удалении проекта при открытом доступе, ошибка и диалог при включении 2FA, страница lock по одноразовой ссылке, страница сброса, файлы системного контейнера, команды reset-super-password и lock-super-session через tsx и сборка их esbuild. - Ограничения: письма не отправлялись (email-канал локально не настроен); OpenAPI-маршрут /api/<procedure> в dev отвечает 500 из-за схемы dockerDiskUsage.getDiskUsage (detailLimit), это было и до изменений. What: - A "Super password" card in Profile: a clear "Not set" warning and a set form (password, confirmation, hint); while access is closed, an unlock form with the hint; while open, a countdown "Access open: 23 h 59 min left" (kept updated, warning under one hour), "Extend to 24 h" and "Close access now"; change, turn off, "Forgot super password? Send reset link" or "Email reset unavailable: configure an email notification, or use SSH". - A global unlock dialog: a tRPC link catches answers with data.superSession = super-session-required, asks for the super password and retries the request; for better-auth endpoints fetchOptions.onError opens the dialog and the user runs the action again. Without a super password the dialog points to Profile → Super password. - /super-password/lock page (no sign-in): "This wasn't me: close access" closes access with the single-use link from the email; it closes on the button press, not on page load, so mail link scanners do not trigger it. - /dashboard/settings/super-password-reset page: sets a new super password with the single-use reset link, only for the same signed-in user. - SSH commands reset-super-password (removes the super password and closes the session, by email or for the owner) and lock-super-session (closes the session), built by esbuild next to reset-password and reset-2fa. Why: - The owner needs a clear way to open, extend and close access, and an emergency reset or lock through email or SSH. Risks: - The UI is English only, like the other profile components (they have no translations). Checks: - Commands and results: bun run typecheck - exit 0; bunx biome check on changed files - no errors; the full test suite passed. The app ran locally (Postgres on OrbStack, dev server on 127.0.0.1): in the browser I checked setting the password (equal to the login password rejected), the dialog on API key creation (wrong password, then the right one, and the key was created by the retry), the countdown with the under-one-hour warning, extend, close, API key writes locked while closed, API key project removal refused while open, the error and dialog on 2FA enable, the lock page with a single-use link, the reset page, system container files, and the reset-super-password and lock-super-session commands through tsx plus their esbuild bundling. - Limitations: no emails were sent (no email channel locally); the OpenAPI route /api/<procedure> answers 500 in dev because of the dockerDiskUsage.getDiskUsage schema (detailLimit), which happens without these changes too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… channels Что: - Новый переключатель уведомлений superPassword (колонка notification.superPassword, по умолчанию false) во всех create/update-схемах и сервисах 12 типов уведомлений. Миграция 0205 (drizzle-kit, со снимком) добавляет колонку и счетчик запросов сброса в super_password. - Оповещения о супер-пароле уходят во все каналы организаций пользователя (owner/admin), где переключатель включен, через существующие отправители: Telegram (с message_thread_id), email, Resend, Discord, Slack, Mattermost, Gotify, ntfy, Lark, Pushover, Teams и custom webhook. События: доступ открыт и продлен (время до конца, IP, user agent и одноразовая ссылка "This wasn't me – close access"; в Telegram это inline-кнопка, а для адреса панели вида localhost или IP - обычная ссылка), закрыт (панель, ссылка или SSH), неверная попытка, блокировка после 5 попыток, супер-пароль задан, изменен, выключен или сброшен (ссылкой или по SSH), запрошена ссылка сброса. - Отправка не блокирует действие: ошибка или зависание канала не мешают unlock и extend. В сообщения не попадают подсказка, хеши и другие секреты. - Сброс через канал: в Profile можно выбрать любой канал с включенным переключателем, и одноразовая ссылка (30 минут, одно использование, хеш в БД) уходит только туда; остальные каналы получают оповещение без ссылки. Не больше 3 ссылок в час на пользователя. Если каналов нет, Profile показывает "No recovery channel: enable 'Super password' on a notification, or use SSH". Страница сброса по-прежнему требует вход тем же пользователем. - Прежний путь только через email удален: email-канал теперь просто один из каналов. - Включение, выключение, смена адресата и удаление канала с этим переключателем объявляются во все каналы с переключателем, включая прежнее состояние измененного канала. Создание, изменение и удаление уведомлений по-прежнему опасные действия. - SSH-команды lock-super-session и reset-super-password тоже отправляют оповещения. Зачем: - У владельца на dev настроен только Telegram-бот, а email нет, поэтому оповещения и восстановление доступа нужны через любой канал уведомлений. Риски: - Отправитель Telegram глушит ошибки API, поэтому неудачная отправка ссылки сброса в Telegram не показывается пользователю как ошибка. - Оповещения идут во все организации, где пользователь owner или admin. Проверки: - Команды и результаты: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-password-alerts.test.ts __test__/security/super-password-telegram.test.ts __test__/security/super-password-service.test.ts - 30 тестов прошли; новые тесты падают на прежнем коде ветки (e7ead79); полный набор: 260 файлов и 2307 тестов прошли, два *.real.test.ts требуют swarm; bun run typecheck - exit 0; bunx biome check по измененным файлам - без ошибок. Локально (Postgres в OrbStack, dev-сервер на 127.0.0.1) проверено: переключатель сохраняется, канал виден в выборе восстановления, ошибка отправки показывается, без каналов виден текст "No recovery channel". - Ограничения: реальные сообщения в Telegram и другие каналы не отправлялись, отправители замоканы (для Telegram проверено тело запроса к Bot API с подменой fetch). What: - New notification toggle superPassword (column notification.superPassword, default false) in every create/update schema and service of the 12 notification types. Migration 0205 (drizzle-kit, with snapshot) adds the column and a reset request counter on super_password. - Super password alerts go to every channel with the toggle on in the organizations where the user is owner or admin, through the existing senders: Telegram (with message_thread_id), email, Resend, Discord, Slack, Mattermost, Gotify, ntfy, Lark, Pushover, Teams and custom webhook. Events: access opened and extended (time left, IP, user agent and a one-time "This wasn't me – close access" link; an inline button in Telegram, a plain link when the panel URL is localhost or an IP), closed (panel, link or SSH), wrong attempt, lockout after 5 attempts, super password set, changed, turned off or reset (by link or over SSH), reset link requested. - Sending is fire-and-forget: a failing or hanging channel never blocks unlock or extend. Messages never carry the hint, hashes or other secrets. - Reset over a channel: Profile lets the user pick any channel with the toggle on, and the one-time link (30 minutes, single use, hashed) goes only there; the other channels get an alert without the link. At most 3 links per hour per user. With no channel, Profile shows "No recovery channel: enable 'Super password' on a notification, or use SSH". The reset page still requires the same signed-in user. - The old email-only path is removed: an email channel is now just one of the channels. - Turning the toggle on or off, redirecting a channel that has it, or deleting it is announced to every channel with the toggle, including the old state of the changed channel. Creating, updating and deleting notifications stay dangerous actions. - The lock-super-session and reset-super-password SSH commands send alerts too. Why: - The owner's dev panel only has a Telegram bot and no email, so alerts and access recovery must work over any notification channel. Risks: - The Telegram sender swallows API errors, so a failed reset link to Telegram is not shown to the user as an error. - Alerts go to every organization where the user is owner or admin. Checks: - Commands and results: CI=true bunx vitest run --config __test__/vitest.config.ts __test__/security/super-password-alerts.test.ts __test__/security/super-password-telegram.test.ts __test__/security/super-password-service.test.ts - 30 tests passed; the new tests fail on the previous branch code (e7ead79); full suite: 260 files and 2307 tests passed, the two *.real.test.ts need swarm; bun run typecheck - exit 0; bunx biome check on changed files - no errors. Locally (Postgres on OrbStack, dev server on 127.0.0.1): the toggle persists, the channel shows in the recovery picker, a send failure is shown, and "No recovery channel" appears without channels. - Limitations: no real messages were sent to Telegram or any other channel; the senders are mocked (for Telegram the Bot API request body was checked with a stubbed fetch). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что: - В блоке "Select the actions" формы уведомлений для всех типов появился переключатель "Super password" с описанием "Security alerts and recovery links for the super password", рядом с App Deploy, Dokploy Restart и другими. Значение загружается при редактировании и отправляется при создании и изменении. Зачем: - Владелец включает оповещения и восстановление супер-пароля на нужном канале, например на своем Telegram-боте. Риски: - Не выявлены: по умолчанию переключатель выключен. Проверки: - Команды и результаты: bun run typecheck - exit 0; bunx biome check по файлу - без ошибок; полный набор тестов прошел. Локально создан канал Gotify с включенным переключателем, значение сохранилось в БД (superPassword = true), канал появился в выборе восстановления в Profile. - Ограничения: в браузере проверен только тип Gotify; остальные типы используют тот же блок формы. What: - The "Select the actions" block of the notification form now has a "Super password" toggle for every type, described as "Security alerts and recovery links for the super password", next to App Deploy, Dokploy Restart and the others. It loads on edit and is sent on create and update. Why: - The owner turns on super password alerts and recovery for a chosen channel, for example their Telegram bot. Risks: - None identified: the toggle is off by default. Checks: - Commands and results: bun run typecheck - exit 0; bunx biome check on the file - no errors; the full test suite passed. Locally I created a Gotify channel with the toggle on; the value was stored (superPassword = true) and the channel showed in the Profile recovery picker. - Limitations: only the Gotify type was clicked through in the browser; the other types share the same form block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this PR about?
Adds a per-user super password. A leaked API key or browser session can no longer write through API keys, reveal secrets or change access without it. Entering it once in Profile opens a 24-hour super session.
This PR includes and supersedes #26 (
fix/docker-logs-traefik-access): its commits are merged in, and its hard denies for Traefik TLS files and panel system containers become "allowed only with an open super session". #26 can be closed after this one lands.Behaviour
FORBIDDEN: "API key write access is locked. Open it in Profile → Super password." Queries still work, and their secrets are already redacted. Browser sessions keep regular writes (create, update, deploy, env edits). Dangerous actions need the super password.superPassword.statusreturns{ isSet, hint, active, expiresAt, lockedUntil, … }, never the hash. It also works through API keys, so the MCP/AI agent can tell why a write failed.Dangerous actions (browser session + open super session)
The central registry is
apps/dokploy/server/api/utils/procedure-access.ts. It classifies all 650 app router procedures as read, write or dangerous. A test fails when a procedure added by an upstream merge is not classified.revealEnvironmentfor application, compose and the 6 databases;docker.getConfig(container env);settings.readTraefikEnv;cluster.addManager/addWorker(swarm join tokens);backup.manualBackupWebServerand web-serverbackup.create/update(dump of the panel DB).acme.json,*.key,dynamic/certificates): read and write for owner/admin only. They stay hidden from the tree and denied while the super session is closed, as in fix: scope container files, service logs and traefik files access #26.user.updatewhen the email or password changes.hooks.before(packages/server/src/lib/super-session-auth.ts):/api-key/*writes, change email/password, delete user, link/unlink, organization member/invitation/role/team endpoints, 2FA enable/disable/backup codes/TOTP URI, passkey register/delete, SSO provider endpoints. Sign-in, sign-out, session refresh, 2FA and passkey verification during sign-in, password reset and session revocation are not gated.dokploy-serverschedules (create, update, run); main Traefik config, env, ports, middlewares, web server Traefik config, dashboard toggle, Traefik file writes; panel domain, IP, update and reload; SSH keys (create, update, delete, save/clean the host key); server create/update/delete; backup and volume backup restore;cleanUnusedVolumes.Container and volume file reads (service containers) count as writes for API keys, so a leaked key without an open super session cannot read files such as
/proc/1/environ.Super password rules
Alerts and recovery over notification channels
notification.superPassword(default off) and wired through all create/update schemas and services.message_thread_id), email, Resend, Discord, Slack, Mattermost, Gotify, ntfy, Lark, Pushover, Teams and custom webhook./super-password/lock. Telegram shows it as an inline URL button; when the panel URL is localhost or a bare IP, which Telegram buttons reject, it is a plain link instead. The page needs no sign-in, can only close access, and closes it on a button press, so link scanners cannot trigger it.getDokployUrl). Alerts never contain the hint, hashes or other secrets.Sample Telegram alert (HTML, with the inline button below it):
Reset and emergency lock
/dashboard/settings/super-password-resetand works only for the same signed-in user. Resetting closes the super session. With no channel, Profile says "No recovery channel: enable 'Super password' on a notification, or use SSH".dokployswarm task):UI
/dashboard/settings/profile: set form, unlock form with hint, "Access open: 17 h 42 min left" countdown (warns under 1 hour), Extend, Close access now, Change, Turn off, and the recovery channel picker or the "No recovery channel" note.data.superSession), a tRPC link asks for the super password and retries the request. For better-auth endpoints the dialog opens, and the user runs the action again.Data
0204_super_password, generated with drizzle-kit. It sorts after every migration inAgentHits-Devand addssuper_password,super_sessionandsuper_password_token(hashed, single-use, expired tokens cleaned up when new ones are created).0205_super_password_alert_channels, also generated with drizzle-kit. It addsnotification.superPassword(boolean, default false) and the reset-link rate limit counters onsuper_password.Checks
CI=true bunx vitest run --config __test__/vitest.config.tson the new and changed test files: service (hashing, same-as-login, hint, 5-attempt lock, extend only while open, token single-use and expiry, emails), tRPC guard (API-key writes locked/open, dangerous via API key always blocked, dangerous from the browser closed/open, conditional procedures, "not set" unchanged), router classification, env upsert REST endpoint, better-auth gated paths, WebSocket terminals (real http server and ws client), Traefik TLS files, and system containers and volumes. Alert tests cover:fetch: chat,message_thread_id, HTML, inline button, plain-link fallback);All pass. They fail on the code before this branch: missing modules, fix: scope container files, service logs and traefik files access #26's hard deny, and the guard or terminal gate switched off. The alert tests fail on the branch before the notification-channel commits.
Full suite: 260 test files and 2307 tests pass. The 2
*.real.test.tsfiles need Docker swarm. They passed separately on a local OrbStack swarm with adokploy-networkoverlay; the swarm and test images were cleaned up afterwards.bun run typecheck: exit 0.bunx biome checkon changed files: 0 errors. The 2 warnings are in pre-existing lines ofserver/wss/*.Manual: ran the app locally (Postgres on OrbStack, dev server on 127.0.0.1). Checked the Profile card, the same-as-login rejection, the unlock dialog with retry on API key creation, the wrong-password counter, the countdown with the under-1-hour warning, extend, close, API-key write lock and dangerous refusal over
/api/trpc, the 2FA endpoint refusal, the lock page, the reset page, the system container file browser, and both CLI commands. esbuild bundles the CLI entries. Notification channels: created a channel with the toggle on and saw it in the Profile recovery picker. A send failure shows a clear error, and with the toggle off Profile shows "No recovery channel". No real messages were sent anywhere: senders are mocked in tests, and the local check used an unresolvable host.Rollout notes
superPassword.statustells the agent whether access is open./api/<procedure>answer 500 because of thedockerDiskUsage.getDiskUsageinput schema (detailLimit). This also happens without this PR.🤖 Generated with Claude Code