Files
ssh-access-tui/docs/ARCHITECTURE.md
T
2026-07-22 14:14:53 +03:00

149 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура и модель угроз
## Почему TUI, а не веб
Веб-панель для управления пользователями требует бэкенда с root-правами
(создание Linux-аккаунтов, правка `authorized_keys`, `sshd_config`),
слушающего порт. Это новый привилегированный сетевой процесс на хосте,
который и так является входной точкой для множества людей: своя
аутентификация/сессии/CSRF, TLS-сертификаты, обновления фреймворка — то есть
дополнительная поверхность атаки, не связанная с самой задачей.
TUI работает поверх уже существующего SSH+`sudo`-доступа администратора к
этому хосту — доступа, который и так есть и уже находится под контролем
(ключи, MFA, аудит доступа к самому jump-хосту). Новых портов, новых
механизмов аутентификации и нового ПО, которое надо патчить, не появляется.
Цена этого решения: нет привычного браузерного UX, и параллельная работа
нескольких админов не даётся бесплатно фреймворком — её пришлось
реализовать вручную (см. «Конкурентность» ниже). Для небольшой команды
администраторов это разумный компромисс.
## Как на самом деле работает ограничение "только прыжок/форвардинг"
Это два независимых слоя, и путать их нельзя:
1. **Сервер (реальная граница безопасности).** Каждому ключу в
`authorized_keys` пользователя приписаны опции:
```
restrict,port-forwarding,permitopen="10.0.1.5:5432",permitopen="10.0.2.9:443" ssh-ed25519 AAAA... user@jump
```
`restrict` разом отключает pty, X11-forwarding, agent-forwarding,
user-rc и port-forwarding; `port-forwarding` точечно возвращает именно
форвардинг (не интерактивный доступ), а `permitopen=` разрешает
форвардинг только к перечисленным `host:port` — это гарантия на уровне
протокола SSH, не зависящая от того, что написано в клиентском конфиге.
Shell пользователя — `/usr/sbin/nologin`: любой запрос shell/exec-канала
(обычный интерактивный `ssh user@jump`) обрывается им же, а
direct-tcpip-каналы (прыжок/форвардинг) идут отдельным путём и
регулируются исключительно `permitopen=`.
Дополнительно в `Match Group ssh-jump-users` (`internal/sysadm/sshdconfig.go`)
продублированы базовые ограничения (`PermitTTY no`, `X11Forwarding no`,
`AllowStreamLocalForwarding no`, локальный, не reverse, forwarding) —
это defense-in-depth на случай, если опции конкретного ключа окажутся
повреждены/потеряны, а не основной механизм.
2. **Клиент (только удобство).** Файл `~/.ssh/config`, который уезжает
пользователю в архиве, — это алиасы (`Host db-prod-1` → `ProxyJump jump`).
Он ничего не запрещает: пользователь может отредактировать его или
вызвать `ssh` руками с любым хостом. Единственная причина, по которой это
не работает, — сервер отклонит `permitopen`, которого нет в списке.
Список хостов в клиентском конфиге должен генерироваться из того же
источника правды (группы в `state.yaml`), что и `permitopen=`, — иначе
конфиг и реальные права разойдутся.
Reverse/remote forwarding (`-R`) в этой модели не выдаётся вообще
(`AllowStreamLocalForwarding no`, никаких `permitlisten=`) — по вашему же
решению при проектировании: этот джамп-хост используется только для входа
внутрь сети, не для проброса портов наружу из неё.
## Жизненный цикл приватного ключа
Приватный ключ **никогда не хранится на диске дольше одной операции
экспорта**: генерируется во временный каталог (`scratch_dir`), упаковывается
в зашифрованный `.7z`, после чего каталог затирается (`shred -u`) и
удаляется — см. `internal/keymgr/pack.go`. В `state.yaml` (и, соответственно,
в git-бэкапе) хранится только **публичная** часть (`PublicKey`) и
fingerprint. Это осознанное решение: кража бэкапа состояния не даёт
приватных ключей ни одного пользователя. Восстановление после потери хоста —
это перевыпуск новых ключей всем (дёшево), а не восстановление старых
приватных ключей (было бы дорого и опасно как хранимый секрет).
Пароль архива — один на партию экспорта (не постоянный, не общий "навсегда"),
генерируется криптографически случайно при каждом экспорте и показывается
администратору один раз. Компрометация этого пароля актуальна только для
ключей, выпущенных в конкретной партии, — передавайте его получателям
отдельным от архива каналом (голосом, в личку, не в общий чат).
## Бэкап и аварийное восстановление
Каталог состояния (`/var/lib/ssh-access-tui/state`) — git-репозиторий.
Каждое изменение (создание пользователя, добавление хоста в группу,
блокировка) — отдельный коммит с сообщением, что и кто сделал. Это даёт:
- бэкап конфигурации (`groups`, `users`, публичные ключи, статусы) — просто
`git push` в удалённый репозиторий (см. `scripts/ssh-access-tui-backup.sh`
+ таймер `ssh-access-tui-backup.timer`, включается инсталлятором);
- историю изменений и `git diff`/`git log` для аудита "кто и когда поменял
разрешённые хосты";
- откат: `git revert`/`git checkout` на предыдущую версию `state.yaml`
при ошибочном изменении (после отката нужно вручную прогнать `resync`
через экран групп, чтобы `authorized_keys` подтянул откаченные права).
При потере хоста: поднимаете новый, разворачиваете `ssh-access-tui`,
клонируете `state.yaml` из бэкапа в `state_dir`, затем для каждого
активного пользователя — заново `useradd` (можно скриптом по списку из
`state.yaml`) и экспорт нового ключа через TUI (старые приватные ключи не
восстановятся и не должны — это ожидаемо, см. выше).
Каталог экспортированных архивов (`export_dir`) **не входит** в этот бэкап и
не должен: это .7z с приватными ключами (пусть и зашифрованными), их место —
у получателей, а не в ещё одной копии на диске/в бэкапе. После подтверждённой
доставки архив стоит удалить с jump-хоста вручную (это сейчас ручной шаг,
см. «Известные ограничения»).
## Журналирование и "кто в сети"
Форвардинг-сессии без pty не создают записей `utmp`, поэтому `who`/`loginctl`
их не видят. Источники правды:
- `LogLevel VERBOSE` в sshd (ставится инсталлятором) — в журнале видно
`Accepted publickey for user from ip`, закрытие сессии и (на VERBOSE)
канал `direct-tcpip` с реальным адресом назначения;
- фоновый systemd-сервис `ssh-access-tui-audit.service`
(`internal/audit/tail.go`) следит за `journalctl -u ssh -f` и пишет
нормализованные события подключения/отключения в
`/var/lib/ssh-access-tui/audit/audit.jsonl` — независимо от ротации
журнала systemd;
- "кто сейчас в сети" — не `who`, а список процессов `sshd: user [priv]`
(`internal/audit/sessions.go`), которые существуют для каждой
аутентифицированной сессии независимо от наличия pty.
## Конкурентность
Инструмент рассчитан на одного администратора одновременно на хосте.
`internal/store.Store.WithLock` берёт эксклюзивную файловую блокировку
(`state.lock`) на время загрузки+изменения+сохранения `state.yaml`: вторая
параллельная сессия получит явную ошибку "state is locked by another
session" вместо тихой порчи данных. Если реально нужно несколько
одновременно работающих админов — потребуется более тонкая блокировка на
уровне отдельных записей и явный git-merge flow; в текущем виде это не
реализовано осознанно (см. вопрос при проектировании).
## Известные ограничения / что дальше
- Обновление клиентского конфига пользователя при появлении **нового**
разрешённого хоста в его группе требует повторного экспорта архива
(что заодно ротирует ключ пользователя — так и задумано: ротация ключа
время от времени — отдельная хорошая практика, просто в MVP она не
отделена от "обновить конфиг"). Удаление хоста из группы вступает в силу
немедленно на сервере (permitopen), без участия клиента.
- Архивы в `export_dir` не удаляются автоматически после доставки —
администратор должен удалить их вручную, когда получатель подтвердил
получение.
- Полностью параллельная работа нескольких админов не поддерживается
(см. «Конкурентность»).