rc
This commit is contained in:
1 parent
e39ad275b5
commit
3d7e3a88f2
14 files changed
+710
-234
No files matched your search
+68
-31
@@ -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
@@ -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-репозиторий.
|
||||
|
||||
Reference in new issue
Block a user