init
This commit is contained in:
commit
23c7133fa9
33 files changed
+2896
No files matched your search
@@ -0,0 +1,148 @@
|
||||
# Архитектура и модель угроз
|
||||
|
||||
## Почему 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` не удаляются автоматически после доставки —
|
||||
администратор должен удалить их вручную, когда получатель подтвердил
|
||||
получение.
|
||||
- Полностью параллельная работа нескольких админов не поддерживается
|
||||
(см. «Конкурентность»).
|
||||
Reference in new issue
Block a user