This commit is contained in:
redlineo committed 2026-07-22 17:13:35 +03:00
1 parent e39ad275b5
commit 3d7e3a88f2
14 files changed
+710 -234

No files matched your search

+68 -31
View File
@@ -17,10 +17,22 @@ sudo ssh-access-tui
Навигация везде: стрелки/`j`/`k` — перемещение по списку, `enter` —
выбрать/подтвердить, `esc` — назад, `ctrl+c` — жёсткий выход в любой момент
(без каких-либо предупреждений — только для аварийных случаев). Обычный
выход через пункт меню «Выход» или `q` в главном меню предупредит, если
есть созданные, но ни разу не экспортированные пользователи (у них ещё нет
ключа) — можно выйти всё равно (`enter`/`y`) или вернуться (`esc`/`n`).
(без каких-либо предупреждений — только для аварийных случаев). `q` как
быстрый выход работает **только в главном меню** (на остальных экранах он
ничего не делает — «esc» уводит назад). Обычный выход (пункт «Выход» или
`q` в главном меню) предупредит, если есть:
- пользователи, для которых ни разу не сгенерирован ключ (не были в
«Экспорт архивов»);
- пользователи, у которых **изменился доступ, но конфиг у них старый**
(кто-то поменял группы/каталог после их последнего экспорта или
обновления конфига) — им нужно прислать новый `config` через «Обновить
конфиги», иначе их локальный конфиг не соответствует их реальным правам.
В обоих случаях можно выйти всё равно (`enter`/`y`) или вернуться
(`esc`/`n`). Главное меню всегда показывает актуальное количество таких
пользователей под списком пунктов, если оно не нулевое — не нужно ждать
выхода, чтобы это заметить.
**Любое деструктивное действие** (блокировка/удаление пользователя,
удаление хоста/сервиса/группы, перевыпуск уже выданного ключа) требует не
@@ -36,8 +48,9 @@ sudo ssh-access-tui
```
Пользователи — создать / заблокировать / разблокировать / удалить
Хосты и сервисы — общий каталог адресов и портов (один раз на всех)
Группы — какие хосты из каталога входят в каждую группу
Группы — какие сервисы из каталога входят в каждую группу
Экспорт архивов — сгенерировать ключи и .7z для доставки
Обновить конфиги — перевыпустить config (без ключа) тем, у кого изменился доступ
Аудит / кто в сети — активные сессии и журнал подключений
Выход
```
@@ -49,12 +62,13 @@ sudo ssh-access-tui
## Модель: каталог → группы → пользователи
Хосты и их сервисы (порты) описываются **один раз** в общем каталоге
(«Хосты и сервисы»), а не заново для каждой группы. Группа — это просто
отметка галочками, какие хосты каталога в неё входят (все сервисы такого
хоста включаются целиком). Если у вас в разных группах фигурируют одни и те
(«Хосты и сервисы»), а не заново для каждой группы. Доступ раздаётся **по
конкретным сервисам**, не по хосту целиком: группа выбирает нужные сервисы
из дерева «хост → его сервисы» — можно взять только `ssh` этого хоста, не
трогая его же `web-admin`. Если у вас в разных группах фигурируют одни и те
же хосты/сервисы — их не нужно набивать повторно, только отметить нужные
для очередной группы. Пользователь состоит в одной или нескольких группах;
его реальный доступ — объединение сервисов всех хостов всех его групп.
его реальный доступ — объединение сервисов, выбранных во всех его группах.
## Сценарий 1: новый сотрудник, у которого уже есть роль/группа доступа
@@ -74,8 +88,12 @@ sudo ssh-access-tui
автоматически обновятся).
4. **Группы** → `n` → имя группы (например `team-billing`), описание
(необязательно) → `enter`.
5. Выбрать группу → `enter` → отметить `space` нужные хосты из каталога →
`enter` сохранить. Список сервисов этих хостов достаётся группе целиком.
5. Выбрать группу → `enter` — откроется дерево «хост → его сервисы».
`space` на строке сервиса отмечает именно его; `space` на строке хоста
(заголовок) отмечает/снимает сразу все его сервисы — удобно, когда нужен
весь хост, но при желании можно оставить только часть его сервисов.
`[x]` — выбрано всё, `[~]` — выбрано частично, `[ ]` — ничего. `enter`
сохранить.
6. **Пользователи** → `n` → имя пользователя (латиницей, будущий Linux-логин)
и комментарий (например ФИО) → `enter`.
7. Отметить `space` нужные группы (можно несколько) → `enter`.
@@ -113,15 +131,26 @@ sudo ssh-access-tui
Если хотите, чтобы у пользователя появился ещё и обновлённый клиентский
`config` с новыми адресами для новых сервисов (иначе он про них просто не
узнает, хотя доступ уже есть) — **Пользователи** → выбрать пользователя →
`c` («обновить конфиг»). Это генерирует маленький архив только с `config` +
`INSTRUCTIONS.txt` (без ключа — ключ у пользователя уже есть и остаётся тем
же), пользователю нужно только подменить файл `config`. Не требует
подтверждения — ключ и доступ этим действием не меняются, это не
`c` («обновить конфиг»), либо (то же самое, но сразу для многих)
**Обновить конфиги** в главном меню. Это генерирует архив только с
`config` + `INSTRUCTIONS.txt` (без ключа — ключ у пользователя уже есть и
остаётся тем же), пользователю нужно только подменить файл `config`. Не
требует подтверждения — ключ и доступ этим действием не меняются, это не
деструктивно.
Полный экспорт (сценарий 1, шаги 8-9) тоже сработает и даст то же самое
плюс заново выпустит ключ (с подтверждением строкой, раз это перевыпуск) —
используйте `c`, когда переиздание ключа не нужно.
**Обновить конфиги** в главном меню — то же самое, что `c`, но пакетно:
список покажет каждого уже выпущенного пользователя с пометкой «конфиг
актуален» или «конфиг устарел — доступ изменился» (см. ниже, как это
отслеживается), можно отметить `space` только устаревших или `a` — всех, и
получить один пароль на всю партию, как при обычном экспорте.
Инструмент сам запоминает, для какого набора сервисов последний раз
выдавался config каждому пользователю (полным экспортом или через
«обновить конфиг»), и сравнивает с тем, что должно быть сейчас — отсюда и
берётся статус «устарел». Полный экспорт (сценарий 1, шаги 8-9) тоже
обновляет этот статус (и заодно перевыпускает ключ, с подтверждением
строкой) — используйте `c`/«Обновить конфиги», когда переиздание ключа не
нужно.
## Сценарий 2: добавили новый сервис/хост, поменялся адрес или порт
@@ -129,26 +158,32 @@ sudo ssh-access-tui
`enter` → `n`.
- Новый хост: **Хосты и сервисы** → `n`, затем добавить ему сервисы.
- Хост сменил адрес, сервис — порт, нужно поправить описание, или
переименовать: `e` на нужной записи. Переименование хоста автоматически
обновляет ссылки на него во всех группах, которые его используют — группы
не «отваливаются» молча.
переименовать: `e` на нужной записи. Переименование хоста или сервиса
автоматически обновляет ссылки на него во всех группах, которые его
используют — группы не «отваливаются» молча и не теряют доступ из-за
простого переименования.
Любое из этих изменений сразу обновляет `permitopen=` в `authorized_keys`
**всех активных пользователей всех групп**, ссылающихся на этот хост, —
**всех активных пользователей всех групп**, ссылающихся на этот сервис, —
доступ появляется немедленно, без каких-либо действий на стороне
пользователя. Клиентский конфиг пользователя (его `~/.ssh/config`) при этом
не обновляется автоматически — для этого используйте **Пользователи** → `c`
(«обновить конфиг», см. сценарий 1а).
пользователя. Их клиентский конфиг (`~/.ssh/config`) при этом не
обновляется автоматически — их локальный файл начинает отставать от
реального доступа (инструмент это заметит и покажет как «конфиг устарел»,
см. выше); чтобы прислать актуальный конфиг — **Обновить конфиги** или `c`
(см. сценарий 1а).
**Группы** → выбрать группу → `enter` — там же можно снять/добавить
привязку группы к хостам каталога (не создавая и не удаляя сами хосты).
**Группы** → выбрать группу → `enter` — то же дерево «хост → сервисы», там
же можно снять/добавить сервисы группе (не создавая и не удаляя сами
хосты/сервисы в каталоге).
Удаление хоста из каталога, сервиса на хосте или целой группы (`d` в
соответствующем списке) — все три действия **деструктивны** (мгновенно
отбирают доступ у всех затронутых пользователей) и требуют набрать
случайную строку подтверждения: экран покажет, что именно удаляется,
сколько пользователей это коснётся, и в каких группах используется
(для хоста/сервиса).
(для хоста/сервиса). Удаление хоста снимает с групп все его сервисы разом;
удаление одного сервиса снимает только его, остальные сервисы того же
хоста в группах остаются.
## Сценарий 3: сотрудник уволился
@@ -202,8 +237,10 @@ sudo ssh-access-tui
когда получатель подтвердил, что распаковал архив (это касается и
архивов от «обновить конфиг», хоть они и не содержат ключ).
- Не обновляет клиентский `~/.ssh/config` пользователя автоматически при
появлении нового сервиса/хоста в его группе — это ручной шаг («обновить
конфиг», сценарий 1а), права доступа на сервере при этом обновляются
немедленно и без участия администратора (см. сценарий 2).
появлении нового сервиса/хоста в его группе — это ручной шаг («Обновить
конфиги», сценарий 1а), хоть инструмент и отслеживает, кому именно он
нужен (главное меню и предупреждение при выходе). Права доступа на
сервере при этом обновляются немедленно и без участия администратора
(см. сценарий 2).
- Не поддерживает одновременную работу нескольких администраторов —
рассчитан на одного за раз (см. «Конкурентность» в ARCHITECTURE.md).
+53 -12
View File
@@ -25,20 +25,26 @@ TUI работает поверх уже существующего SSH+`sudo`-
раз, а не заново в каждой группе. Хост (`Host`) — это адрес (например,
`172.16.1.20`) с описанием; на одном хосте может быть несколько сервисов
(`Service`) — независимых TCP-портов: `ssh` на 22, какое-то приложение на
7888, и так далее. Группа (`Group`) не хранит хосты сама — она хранит
только список **имён** хостов из каталога (`Group.Hosts []string`),
которые в неё входят (все сервисы такого хоста включаются целиком). Если
одни и те же хосты/сервисы нужны нескольким группам — они описываются один
раз в каталоге, а группы просто отмечают их галочками, вместо повторного
набора адресов и портов.
7888, и так далее. Группа (`Group`) не хранит хосты — она хранит список
**ссылок на конкретные сервисы** каталога (`Group.Services []ServiceRef`,
каждая — пара `{Host, Service}`). Доступ раздаётся по сервису, а не по
хосту целиком: группа может включать `db1/ssh`, не давая заодно
`db1/web-admin` того же хоста. UI показывает выбор как дерево "хост →
сервисы" (`internal/ui/groups.go`), где отметка на строке хоста — это
просто ярлык "выбрать все его сервисы", а не отдельный уровень доступа в
модели. Если одни и те же хосты/сервисы нужны нескольким группам — они
описываются один раз в каталоге, а группы просто отмечают нужные сервисы,
вместо повторного набора адресов и портов.
Пользователь может состоять в нескольких группах; его реальный эффективный
доступ — объединение сервисов всех хостов всех его групп, это и называется
`Forward` (`internal/model/types.go`, `State.AllowedForwards`). Переименование
хоста автоматически обновляет ссылки на него во всех группах
(`State.RenameHost`); удаление хоста снимает его со всех групп, которые его
использовали (`State.DeleteHost`) — ссылочная целостность каталога держится
в модели, а не проверяется вручную в UI.
доступ — объединение сервисов, на которые ссылаются все его группы, это и
называется `Forward` (`internal/model/types.go`, `State.AllowedForwards`).
Ссылочная целостность каталога держится в модели, а не проверяется вручную
в UI: переименование хоста или сервиса обновляет `ServiceRef` во всех
группах (`State.RenameHost`, `State.RenameServiceRefs`); удаление хоста или
одного его сервиса снимает соответствующие `ServiceRef` со всех групп
(`State.DeleteHost`, `State.RemoveServiceRefs`) — переименование/удаление
одного сервиса никогда не задевает остальные сервисы того же хоста.
Ключевой момент: пользователь **никогда не подключается по SSH к целевому
хосту напрямую** (ни через `ProxyJump`, ни как-то ещё) — он открывает одно
@@ -110,6 +116,31 @@ Reverse/remote forwarding (`-R`) в этой модели не выдаётся
порты. Итоговое соответствие "локальный порт → сервис" попадает в
`INSTRUCTIONS.txt` внутри архива пользователя.
## Отслеживание устаревших конфигов
Права на сервере (`permitopen=` в `authorized_keys`) обновляются немедленно
при любом изменении групп/каталога — но клиентский `config` пользователя
лежит у него на компьютере и сам не меняется. Чтобы админ не забывал, кому
нужно прислать новый `config`, каждому пользователю в состоянии хранится
`ForwardsHash` (`internal/model/hash.go`) — детерминированный (порядок
форвардов не важен, список сортируется перед хешированием) хэш набора
`Forward`, который был у него на момент последнего экспорта или "обновить
конфиг". `State.IsConfigStale` просто сравнивает этот сохранённый хэш с
хэшем текущего `AllowedForwards(u)`: если они разошлись — где-то в
группах/каталоге, от которых зависит доступ этого пользователя, что-то
поменялось, а он ещё не получил обновлённый `config`.
Это используется в двух местах: в главном меню (живой счётчик под списком
пунктов, пересчитывается на каждый кадр — `internal/ui/menu.go`,
`viewMenu`) и на экране подтверждения выхода (`attemptQuit`/
`viewQuitConfirm`), отдельно от пользователей без ключа вообще
(`State.NeverExportedUsernames`). "Обновить конфиги"
(`internal/ui/configupdate.go`) — пакетный аналог экспорта, но без
переиздания ключа: строит только `config`+`INSTRUCTIONS.txt` для каждого
выбранного пользователя (`keymgr.BuildConfigOnlyArchive`) и обновляет его
`ForwardsHash`, ничего не трогая в `authorized_keys` и не требуя
подтверждения строкой — это не деструктивное действие.
## Жизненный цикл приватного ключа
Приватный ключ **никогда не хранится на диске дольше одной операции
@@ -141,6 +172,16 @@ fingerprint. Это осознанное решение: кража бэкапа
у которого его ещё не было, ничего не ломает и подтверждения не требует —
трение нужно только там, где есть что сломать.
**Готча при добавлении новых экранов-списков:** `bubbles/list` по умолчанию
сам перехватывает `q` и мгновенно возвращает `tea.Quit`, в обход всей этой
логики — на любом экране, где необработанная клавиша проваливается в
`list.Model.Update`. `internal/ui/app.go` (`newList`) вызывает
`l.DisableQuitKeybindings()` на каждом создаваемом списке ровно из-за
этого — при добавлении нового списка мимо `newList` эта защита пропадает
незаметно (сам список продолжит работать, просто `q` будет закрывать
программу без единого предупреждения). Обнаружено и подтверждено тестом
именно в такой форме: экран без нужного `case "q"` в своём `Update`.
## Бэкап и аварийное восстановление
Каталог состояния (`/var/lib/ssh-access-tui/state`) — git-репозиторий.