This commit is contained in:
redlineo committed 2026-07-22 14:14:53 +03:00
commit 23c7133fa9
33 files changed
+2896

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
# Руководство администратора
Практическое руководство по экранам `ssh-access-tui` и типовым сценариям.
Архитектурные решения и модель угроз — в [ARCHITECTURE.md](ARCHITECTURE.md).
## Запуск
```sh
sudo ssh-access-tui
```
Требует root (создаёт/блокирует системных пользователей, правит
`authorized_keys`). Инструмент рассчитан на **одного** администратора
одновременно на хосте — если кто-то ещё уже открыл TUI, вторая сессия
получит ошибку про занятую блокировку состояния (`state.lock`), а не молча
перезапишет чужие изменения.
Навигация везде: стрелки/`j`/`k` — перемещение по списку, `enter` —
выбрать/подтвердить, `esc` — назад, `ctrl+c` — выход в любой момент.
## Главное меню
```
Пользователи — создать / заблокировать / разблокировать / удалить
Группы и хосты — какие внутренние хосты кому разрешены
Экспорт архивов — сгенерировать ключи и .7z для доставки
Аудит / кто в сети — активные сессии и журнал подключений
Выход
```
## Сценарий 1: новый сотрудник, у которого уже есть роль/группа доступа
Обычно сначала настраивают группу (один раз), потом заводят пользователей в
неё пачками.
1. **Группы и хосты** → `n` → ввести имя группы (например `team-billing`) →
`enter`.
2. Выбрать группу → `enter`, попасть в список хостов группы (изначально
пуст).
3. `n` → заполнить: имя хоста (короткий алиас, например `db-prod-1`), адрес
(как виден с jump-хоста), порт, описание (необязательно) → `tab` между
полями → `enter` сохранить. Повторить для каждого внутреннего хоста,
который должен быть доступен этой группе.
4. `esc` дважды — назад в главное меню.
5. **Пользователи** → `n` → имя пользователя (латиницей, будущий Linux-логин)
и комментарий (например ФИО) → `enter`.
6. Отметить `space` нужные группы (можно несколько — эффективный список
хостов пользователя = объединение хостов всех отмеченных групп) → `enter`.
Это уже создаёт системный Linux-аккаунт (без пароля, без интерактивного
шелла) и запись в состоянии. **Ключ ещё не выдан** — статус в списке
пользователей покажет «ключ не выдан».
7. **Экспорт архивов** → отметить `space` только что созданного пользователя
(или `a` — отметить всех, удобно при пакетном найме) → `enter`.
8. Инструмент сгенерирует ключ, пропишет `permitopen=` под текущие хосты
группы, соберёт `.7z` и покажет **один общий пароль на всю партию** — он
показывается один раз, запишите/скопируйте сразу.
9. Передайте каждому пользователю его архив (файл, путь показан на экране,
лежит в `export_dir`) любым каналом, а пароль от архива — **отдельным**
каналом (голосом, в личку) и по возможности не всем одним и тем же
сообщением в общем чате.
10. Пользователь распаковывает архив на Windows по инструкции
`INSTRUCTIONS.txt` внутри архива (она уже сгенерирована под его
конкретные разрешённые хосты).
## Сценарий 2: добавили новый разрешённый хост существующей группе
1. **Группы и хосты** → выбрать группу → `enter`.
2. `n` → заполнить новый хост → `enter`.
Это сразу обновляет `permitopen=` в `authorized_keys` **всех активных
пользователей** этой группы — доступ появляется немедленно, без каких-либо
действий на стороне пользователя. Клиентский конфиг пользователя (его
`~/.ssh/config`) при этом не обновляется автоматически — если хотите, чтобы
у пользователя появился готовый алиас для нового хоста, а не только "голый"
доступ, повторно прогоните его через **Экспорт архивов** (это заодно
ротирует его ключ — считайте это плановой ротацией, не проблемой).
Удаление хоста из группы (`d` в списке хостов) действует так же немедленно
и в обратную сторону.
## Сценарий 3: сотрудник уволился
1. **Пользователи** → выбрать пользователя → `l` (заблокировать).
Это одновременно: блокирует пароль аккаунта, устанавливает немедленный срок
истечения аккаунта (`chage -E 1`) и комментирует его ключ в
`authorized_keys` — три независимых механизма, чтобы блокировка не зависела
от одного из них. Статус в списке сменится на `locked`.
Если сотрудник вернулся — `u` (разблокировать) на том же пользователе:
снимает блокировку аккаунта и восстанавливает его ключ в `authorized_keys`
с текущими правами групп.
Полное удаление аккаунта (`d`) — необратимо (`userdel -r`, домашняя папка
стирается), используйте для окончательного оффбординга, не для временной
блокировки.
## Сценарий 4: проверить, кто сейчас подключён / посмотреть историю
**Аудит / кто в сети** показывает:
- «Сейчас в сети» — список активных SSH-сессий (по процессам `sshd:
user [priv]`, а не по `who`, потому что форвардинг-сессии без
интерактивного терминала не создают обычных login-записей);
- «Последние события» — журнал подключений/отключений, который в фоне
собирает `ssh-access-tui-audit.service` из `journalctl`, независимо от
ротации системного журнала.
`r` — обновить оба списка. Если журнал подключений пуст, хотя пользователи
явно заходили — проверьте, что фоновый сервис запущен:
`systemctl status ssh-access-tui-audit.service`.
## Сценарий 5: перенос на новый хост / хост умер
См. подробности в [ARCHITECTURE.md](ARCHITECTURE.md#бэкап-и-аварийное-восстановление).
Коротко: состояние (`groups`, `users`, публичные ключи, статусы) —
git-репозиторий, бэкапится часовым таймером в удалённый git. На новом хосте:
поднять `ssh-access-tui`, влить `state.yaml` из бэкапа, создать системные
Linux-аккаунты для активных пользователей, выпустить им новые ключи через
**Экспорт архивов** (старые приватные ключи специально нигде не хранятся —
это не баг, см. архитектурный документ).
## Что не делает эта версия (нужно руками)
- Не удаляет `.7z` из `export_dir` после доставки — сделайте это сами,
когда получатель подтвердил, что распаковал архив.
- Не обновляет клиентский `~/.ssh/config` пользователя автоматически при
появлении нового хоста в его группе — только права доступа на сервере
обновляются немедленно (см. сценарий 2).
- Не поддерживает одновременную работу нескольких администраторов —
рассчитан на одного за раз (см. «Конкурентность» в ARCHITECTURE.md).
+148
View File
@@ -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` не удаляются автоматически после доставки —
администратор должен удалить их вручную, когда получатель подтвердил
получение.
- Полностью параллельная работа нескольких админов не поддерживается
(см. «Конкурентность»).