add edit and all in one menu for hosts and services
This commit is contained in:
1 parent
8c1b93c14e
commit
e39ad275b5
15 files changed
+1136
-675
No files matched your search
+99
-81
@@ -22,74 +22,85 @@ sudo ssh-access-tui
|
||||
есть созданные, но ни разу не экспортированные пользователи (у них ещё нет
|
||||
ключа) — можно выйти всё равно (`enter`/`y`) или вернуться (`esc`/`n`).
|
||||
|
||||
Любое деструктивное действие (блокировка/удаление пользователя, удаление
|
||||
хоста или сервиса) сначала показывает отдельный экран подтверждения с
|
||||
пояснением последствий — ничего не происходит до `enter`/`y`; `esc`/`n`
|
||||
отменяет без каких-либо изменений.
|
||||
**Любое деструктивное действие** (блокировка/удаление пользователя,
|
||||
удаление хоста/сервиса/группы, перевыпуск уже выданного ключа) требует не
|
||||
`enter`/`y`, а **набрать вручную короткую случайную строку**, которую
|
||||
инструмент показывает на экране подтверждения (например `kqzvm-482`) — она
|
||||
каждый раз новая. Неверный ввод просто откатывает к полю ввода с пояснением
|
||||
«не совпадает», ничего не меняя; `esc` отменяет действие полностью в любой
|
||||
момент. Это защита именно от «промахнулся мимо enter» — набрать чужую
|
||||
случайную строку по ошибке невозможно.
|
||||
|
||||
## Главное меню
|
||||
|
||||
```
|
||||
Пользователи — создать / заблокировать / разблокировать / удалить
|
||||
Группы и хосты — какие внутренние хосты кому разрешены
|
||||
Хосты и сервисы — общий каталог адресов и портов (один раз на всех)
|
||||
Группы — какие хосты из каталога входят в каждую группу
|
||||
Экспорт архивов — сгенерировать ключи и .7z для доставки
|
||||
Аудит / кто в сети — активные сессии и журнал подключений
|
||||
Выход
|
||||
```
|
||||
|
||||
Имена групп, хостов и сервисов должны быть без пробелов (латиница, цифры,
|
||||
Имена хостов, сервисов и групп должны быть без пробелов (латиница, цифры,
|
||||
`. _ -`) — они попадают в конфиг и в `authorized_keys`; инструмент откажет с
|
||||
понятной ошибкой, если ввести имя с пробелом.
|
||||
|
||||
## Модель: каталог → группы → пользователи
|
||||
|
||||
Хосты и их сервисы (порты) описываются **один раз** в общем каталоге
|
||||
(«Хосты и сервисы»), а не заново для каждой группы. Группа — это просто
|
||||
отметка галочками, какие хосты каталога в неё входят (все сервисы такого
|
||||
хоста включаются целиком). Если у вас в разных группах фигурируют одни и те
|
||||
же хосты/сервисы — их не нужно набивать повторно, только отметить нужные
|
||||
для очередной группы. Пользователь состоит в одной или нескольких группах;
|
||||
его реальный доступ — объединение сервисов всех хостов всех его групп.
|
||||
|
||||
## Сценарий 1: новый сотрудник, у которого уже есть роль/группа доступа
|
||||
|
||||
Обычно сначала настраивают группу (один раз), потом заводят пользователей в
|
||||
неё пачками. Иерархия: **группа** → **хосты** → **сервисы (порты) на хосте**
|
||||
— на одном хосте может быть несколько сервисов (например, `ssh` на 22 и
|
||||
`web-api` на 7888), каждый добавляется отдельно.
|
||||
Обычно каталог и группы настраивают заранее (или один раз добавляют то,
|
||||
чего не хватает), потом заводят пользователей пачками.
|
||||
|
||||
1. **Группы и хосты** → `n` → ввести имя группы (например `team-billing`) →
|
||||
`enter`.
|
||||
2. Выбрать группу → `enter`, попасть в список хостов группы (изначально
|
||||
пуст).
|
||||
3. `n` → заполнить: имя хоста (короткое, например `db-prod-1`), адрес (как
|
||||
виден с jump-хоста) → `tab` между полями → `enter`. Сразу после этого
|
||||
попадаете в список сервисов этого хоста (тоже изначально пуст).
|
||||
4. `n` → заполнить сервис: имя (например `ssh` или `web-api`), порт на
|
||||
хосте (например `22` или `7888`), описание (необязательно) → `enter`.
|
||||
Повторите `n` столько раз, сколько сервисов есть на этом хосте.
|
||||
5. `esc` — назад к списку хостов группы; повторите шаги 3-4 для остальных
|
||||
хостов группы, затем `esc` дважды — назад в главное меню.
|
||||
1. **Хосты и сервисы** → `n` → имя хоста (например `db-prod-1`), адрес (как
|
||||
виден с jump-хоста), описание (необязательно) → `enter`. Сразу
|
||||
открывается список сервисов этого хоста (изначально пуст).
|
||||
2. `n` → имя сервиса (например `ssh` или `web-api`), порт на хосте
|
||||
(например `22` или `7888`), описание (необязательно) → `enter`. Повторите
|
||||
`n` для каждого сервиса на этом хосте.
|
||||
3. `esc` — назад к списку хостов; повторите шаги 1-2 для остальных хостов,
|
||||
затем `esc` — в главное меню. Если хост/сервис уже существует и нужно
|
||||
только поправить адрес/порт/описание — `e` вместо `n` на нужной записи
|
||||
(можно поменять и имя — все группы, которые ссылались на старое имя,
|
||||
автоматически обновятся).
|
||||
4. **Группы** → `n` → имя группы (например `team-billing`), описание
|
||||
(необязательно) → `enter`.
|
||||
5. Выбрать группу → `enter` → отметить `space` нужные хосты из каталога →
|
||||
`enter` сохранить. Список сервисов этих хостов достаётся группе целиком.
|
||||
6. **Пользователи** → `n` → имя пользователя (латиницей, будущий Linux-логин)
|
||||
и комментарий (например ФИО) → `enter`.
|
||||
7. Отметить `space` нужные группы (можно несколько — эффективный доступ
|
||||
пользователя = объединение сервисов всех хостов всех отмеченных групп) →
|
||||
`enter`.
|
||||
7. Отметить `space` нужные группы (можно несколько) → `enter`.
|
||||
|
||||
Это уже создаёт системный Linux-аккаунт (без пароля, без интерактивного
|
||||
шелла) и запись в состоянии. **Ключ ещё не выдан** — статус в списке
|
||||
пользователей покажет «ключ не выдан».
|
||||
|
||||
8. **Экспорт архивов** → отметить `space` только что созданного пользователя
|
||||
(или `a` — отметить всех, удобно при пакетном найме) → `enter`. Список
|
||||
сразу показывает у каждого пользователя, будет ли выдан новый ключ или
|
||||
существующий будет **перевыпущен** (актуально, если случайно попадёт в
|
||||
выборку уже работающий пользователь).
|
||||
9. Экран подтверждения: для тех, у кого ключ уже был, явно предупредит, что
|
||||
старый архив перестанет работать — только после `enter`/`y` (либо `esc`/`n`
|
||||
для отмены, тогда не создаётся и не меняется вообще ничего) инструмент
|
||||
сгенерирует ключ, пропишет `permitopen=` под текущие сервисы группы и
|
||||
соберёт `.7z`, показав **один общий пароль на всю партию** — он
|
||||
показывается один раз, запишите/скопируйте сразу.
|
||||
10. Передайте каждому пользователю его архив (файл, путь показан на экране,
|
||||
лежит в `export_dir`) любым каналом, а пароль от архива — **отдельным**
|
||||
каналом (голосом, в личку) и по возможности не всем одним и тем же
|
||||
сообщением в общем чате.
|
||||
11. Пользователь распаковывает архив на Windows по инструкции
|
||||
`INSTRUCTIONS.txt` внутри архива — она уже на простом русском языке,
|
||||
без технических терминов: просто список "локальный адрес → какой это
|
||||
сервис → куда он реально ведёт", составленный под конкретные разрешённые
|
||||
сервисы этого пользователя.
|
||||
(или `a` — отметить всех, удобно при пакетном найме) → `enter`. Для
|
||||
пользователей без ключа архив генерируется сразу, без лишних вопросов —
|
||||
это не деструктивное действие. Инструмент сгенерирует ключ, пропишет
|
||||
`permitopen=` под текущие сервисы группы и соберёт `.7z`, показав **один
|
||||
общий пароль на всю партию** — он показывается один раз,
|
||||
запишите/скопируйте сразу.
|
||||
9. Передайте каждому пользователю его архив (файл, путь показан на экране,
|
||||
лежит в `export_dir`) любым каналом, а пароль от архива — **отдельным**
|
||||
каналом (голосом, в личку) и по возможности не всем одним и тем же
|
||||
сообщением в общем чате.
|
||||
10. Пользователь распаковывает архив на Windows по инструкции
|
||||
`INSTRUCTIONS.txt` внутри архива — она на простом русском языке, без
|
||||
технических терминов: положить файлы в папку, ввести одну команду и
|
||||
держать окно открытым, дальше список «такой адрес у вас локально → что
|
||||
это → где реально находится», составленный под конкретные сервисы этого
|
||||
пользователя.
|
||||
|
||||
## Сценарий 1а: изменить группы уже существующего пользователя
|
||||
|
||||
@@ -100,42 +111,49 @@ sudo ssh-access-tui
|
||||
(изменение групп никогда не ротирует ключ).
|
||||
|
||||
Если хотите, чтобы у пользователя появился ещё и обновлённый клиентский
|
||||
`config` с новыми `LocalForward` для новых сервисов (иначе он про них
|
||||
просто не узнает, хотя доступ уже есть) — **Пользователи** → выбрать
|
||||
пользователя → `c` («обновить конфиг»). Это генерирует маленький архив
|
||||
только с `config` + `INSTRUCTIONS.txt` (без ключа — ключ у пользователя уже
|
||||
есть и остаётся тем же), пользователю нужно только подменить файл `config`.
|
||||
Не требует подтверждения — ключ и доступ этим действием не меняются, это не
|
||||
`config` с новыми адресами для новых сервисов (иначе он про них просто не
|
||||
узнает, хотя доступ уже есть) — **Пользователи** → выбрать пользователя →
|
||||
`c` («обновить конфиг»). Это генерирует маленький архив только с `config` +
|
||||
`INSTRUCTIONS.txt` (без ключа — ключ у пользователя уже есть и остаётся тем
|
||||
же), пользователю нужно только подменить файл `config`. Не требует
|
||||
подтверждения — ключ и доступ этим действием не меняются, это не
|
||||
деструктивно.
|
||||
|
||||
Полный экспорт (со сценария 1, шаги 8-9) тоже сработает и даст то же самое
|
||||
плюс заново выпустит ключ — используйте `c`, когда переиздание ключа не
|
||||
нужно (обычный случай при простом изменении групп).
|
||||
Полный экспорт (сценарий 1, шаги 8-9) тоже сработает и даст то же самое
|
||||
плюс заново выпустит ключ (с подтверждением строкой, раз это перевыпуск) —
|
||||
используйте `c`, когда переиздание ключа не нужно.
|
||||
|
||||
## Сценарий 2: добавили новый сервис/хост существующей группе
|
||||
## Сценарий 2: добавили новый сервис/хост, поменялся адрес или порт
|
||||
|
||||
1. **Группы и хосты** → выбрать группу → `enter`.
|
||||
2. Либо `n` — новый хост (затем добавить ему хотя бы один сервис), либо
|
||||
выбрать существующий хост → `enter` → `n` — новый сервис на нём.
|
||||
- Новый сервис на существующем хосте: **Хосты и сервисы** → выбрать хост →
|
||||
`enter` → `n`.
|
||||
- Новый хост: **Хосты и сервисы** → `n`, затем добавить ему сервисы.
|
||||
- Хост сменил адрес, сервис — порт, нужно поправить описание, или
|
||||
переименовать: `e` на нужной записи. Переименование хоста автоматически
|
||||
обновляет ссылки на него во всех группах, которые его используют — группы
|
||||
не «отваливаются» молча.
|
||||
|
||||
Это сразу обновляет `permitopen=` в `authorized_keys` **всех активных
|
||||
пользователей** этой группы — доступ появляется немедленно, без каких-либо
|
||||
действий на стороне пользователя. Клиентский конфиг пользователя (его
|
||||
`~/.ssh/config`) при этом не обновляется автоматически — если хотите, чтобы
|
||||
у пользователя появилась готовая строка `LocalForward` для нового сервиса, а
|
||||
не только "голый" доступ на сервере, используйте **Пользователи** → `c`
|
||||
(«обновить конфиг») — ключ при этом не меняется (см. сценарий 1а).
|
||||
Любое из этих изменений сразу обновляет `permitopen=` в `authorized_keys`
|
||||
**всех активных пользователей всех групп**, ссылающихся на этот хост, —
|
||||
доступ появляется немедленно, без каких-либо действий на стороне
|
||||
пользователя. Клиентский конфиг пользователя (его `~/.ssh/config`) при этом
|
||||
не обновляется автоматически — для этого используйте **Пользователи** → `c`
|
||||
(«обновить конфиг», см. сценарий 1а).
|
||||
|
||||
Удаление сервиса или целого хоста (`d` в соответствующем списке) — оба
|
||||
действия **деструктивны** (мгновенно отбирают доступ у всех пользователей
|
||||
группы) и требуют подтверждения: экран покажет, что именно удаляется и
|
||||
скольких пользователей это коснётся, `enter`/`y` подтверждает, `esc`/`n`
|
||||
отменяет без каких-либо изменений.
|
||||
**Группы** → выбрать группу → `enter` — там же можно снять/добавить
|
||||
привязку группы к хостам каталога (не создавая и не удаляя сами хосты).
|
||||
|
||||
Удаление хоста из каталога, сервиса на хосте или целой группы (`d` в
|
||||
соответствующем списке) — все три действия **деструктивны** (мгновенно
|
||||
отбирают доступ у всех затронутых пользователей) и требуют набрать
|
||||
случайную строку подтверждения: экран покажет, что именно удаляется,
|
||||
сколько пользователей это коснётся, и в каких группах используется
|
||||
(для хоста/сервиса).
|
||||
|
||||
## Сценарий 3: сотрудник уволился
|
||||
|
||||
1. **Пользователи** → выбрать пользователя → `l` (заблокировать) → экран
|
||||
подтверждения объяснит, что произойдёт → `enter`/`y`.
|
||||
1. **Пользователи** → выбрать пользователя → `l` (заблокировать) → набрать
|
||||
строку подтверждения, которую покажет экран → `enter`.
|
||||
|
||||
Блокировка — это одновременно: блокирует пароль аккаунта, устанавливает
|
||||
немедленный срок истечения аккаунта (`chage -E 1`) и комментирует его ключ в
|
||||
@@ -148,10 +166,10 @@ sudo ssh-access-tui
|
||||
требует, срабатывает сразу.
|
||||
|
||||
Полное удаление аккаунта (`d`) — **необратимо** (`userdel -r`, домашняя папка
|
||||
стирается) и тоже требует подтверждения (экран явно предупредит, что это
|
||||
необратимо и что для временного отключения лучше использовать блокировку).
|
||||
Используйте удаление только для окончательного оффбординга, не для
|
||||
временной блокировки.
|
||||
стирается) и тоже требует подтверждения строкой (экран явно предупредит,
|
||||
что это необратимо и что для временного отключения лучше использовать
|
||||
блокировку). Используйте удаление только для окончательного оффбординга, не
|
||||
для временной блокировки.
|
||||
|
||||
## Сценарий 4: проверить, кто сейчас подключён / посмотреть историю
|
||||
|
||||
@@ -171,12 +189,12 @@ sudo ssh-access-tui
|
||||
## Сценарий 5: перенос на новый хост / хост умер
|
||||
|
||||
См. подробности в [ARCHITECTURE.md](ARCHITECTURE.md#бэкап-и-аварийное-восстановление).
|
||||
Коротко: состояние (`groups`, `users`, публичные ключи, статусы) —
|
||||
git-репозиторий, бэкапится часовым таймером в удалённый git. На новом хосте:
|
||||
поднять `ssh-access-tui`, влить `state.yaml` из бэкапа, создать системные
|
||||
Linux-аккаунты для активных пользователей, выпустить им новые ключи через
|
||||
**Экспорт архивов** (старые приватные ключи специально нигде не хранятся —
|
||||
это не баг, см. архитектурный документ).
|
||||
Коротко: состояние (каталог хостов/сервисов, группы, пользователи,
|
||||
публичные ключи, статусы) — git-репозиторий, бэкапится часовым таймером в
|
||||
удалённый git. На новом хосте: поднять `ssh-access-tui`, влить `state.yaml`
|
||||
из бэкапа, создать системные Linux-аккаунты для активных пользователей,
|
||||
выпустить им новые ключи через **Экспорт архивов** (старые приватные ключи
|
||||
специально нигде не хранятся — это не баг, см. архитектурный документ).
|
||||
|
||||
## Что не делает эта версия (нужно руками)
|
||||
|
||||
|
||||
+50
-25
@@ -19,15 +19,26 @@ TUI работает поверх уже существующего SSH+`sudo`-
|
||||
реализовать вручную (см. «Конкурентность» ниже). Для небольшой команды
|
||||
администраторов это разумный компромисс.
|
||||
|
||||
## Модель доступа: Host → Service → Forward
|
||||
## Модель доступа: каталог хостов → группы → Forward
|
||||
|
||||
Хост (`Host`) — это адрес (например, `172.16.1.20`). На одном хосте может
|
||||
быть несколько сервисов (`Service`) — независимых TCP-портов: `ssh` на 22,
|
||||
какое-то приложение на 7888, и так далее. Группа (`Group`) — это набор
|
||||
хостов (со всеми их сервисами), пользователь может состоять в нескольких
|
||||
группах; его реальный эффективный доступ — объединение сервисов всех хостов
|
||||
всех его групп, это и называется `Forward` (`internal/model/types.go`,
|
||||
`State.AllowedForwards`).
|
||||
Хосты и их сервисы — **общий каталог** (`State.Hosts`), определённый один
|
||||
раз, а не заново в каждой группе. Хост (`Host`) — это адрес (например,
|
||||
`172.16.1.20`) с описанием; на одном хосте может быть несколько сервисов
|
||||
(`Service`) — независимых TCP-портов: `ssh` на 22, какое-то приложение на
|
||||
7888, и так далее. Группа (`Group`) не хранит хосты сама — она хранит
|
||||
только список **имён** хостов из каталога (`Group.Hosts []string`),
|
||||
которые в неё входят (все сервисы такого хоста включаются целиком). Если
|
||||
одни и те же хосты/сервисы нужны нескольким группам — они описываются один
|
||||
раз в каталоге, а группы просто отмечают их галочками, вместо повторного
|
||||
набора адресов и портов.
|
||||
|
||||
Пользователь может состоять в нескольких группах; его реальный эффективный
|
||||
доступ — объединение сервисов всех хостов всех его групп, это и называется
|
||||
`Forward` (`internal/model/types.go`, `State.AllowedForwards`). Переименование
|
||||
хоста автоматически обновляет ссылки на него во всех группах
|
||||
(`State.RenameHost`); удаление хоста снимает его со всех групп, которые его
|
||||
использовали (`State.DeleteHost`) — ссылочная целостность каталога держится
|
||||
в модели, а не проверяется вручную в UI.
|
||||
|
||||
Ключевой момент: пользователь **никогда не подключается по SSH к целевому
|
||||
хосту напрямую** (ни через `ProxyJump`, ни как-то ещё) — он открывает одно
|
||||
@@ -85,17 +96,19 @@ Reverse/remote forwarding (`-R`) в этой модели не выдаётся
|
||||
|
||||
Пользователю нужно пробросить сразу много сервисов за одно соединение
|
||||
(например, 10 хостов, на каждом порт 22 и порт 7888 — 20 форвардов сразу).
|
||||
Локально на его машине два разных удалённых назначения не могут занимать
|
||||
один и тот же локальный порт одновременно, а несколько хостов с одинаковым
|
||||
портом (22 или 7888 у всех десяти) — обычная ситуация. Поэтому локальный
|
||||
порт **не всегда равен удалённому**: `internal/keymgr/portalloc.go`
|
||||
(`AllocateLocalPorts`) для каждого форварда сначала пробует занять локальный
|
||||
порт, совпадающий с удалённым (удобно и предсказуемо, когда порт встречается
|
||||
один раз), а при конфликте — берёт следующий свободный из диапазона `20000+`.
|
||||
Присвоение детерминировано (зависит только от порядка и состава форвардов
|
||||
пользователя), поэтому повторный экспорт архива с тем же набором хостов даёт
|
||||
те же локальные порты. Итоговое соответствие "локальный порт → сервис"
|
||||
попадает в `INSTRUCTIONS.txt` внутри архива пользователя.
|
||||
Локальный порт **никогда не равен удалённому порту** — не только чтобы
|
||||
избежать коллизий между несколькими хостами с одинаковым портом (обычная
|
||||
ситуация: 22 или 7888 у всех десяти), но и потому, что многие удалённые
|
||||
порты привилегированные (`< 1024`, в первую очередь сам ssh на 22) —
|
||||
слушателя на таком порту локально можно открыть только с правами
|
||||
администратора, которых у обычного пользователя на его Windows-машине нет.
|
||||
`internal/keymgr/portalloc.go` (`AllocateLocalPorts`) поэтому просто выдаёт
|
||||
локальные порты последовательно из диапазона `20000+`, без попытки
|
||||
переиспользовать номер удалённого порта. Присвоение детерминировано
|
||||
(зависит только от порядка и состава форвардов пользователя), поэтому
|
||||
повторный экспорт архива с тем же набором хостов даёт те же локальные
|
||||
порты. Итоговое соответствие "локальный порт → сервис" попадает в
|
||||
`INSTRUCTIONS.txt` внутри архива пользователя.
|
||||
|
||||
## Жизненный цикл приватного ключа
|
||||
|
||||
@@ -115,6 +128,19 @@ fingerprint. Это осознанное решение: кража бэкапа
|
||||
ключей, выпущенных в конкретной партии, — передавайте его получателям
|
||||
отдельным от архива каналом (голосом, в личку, не в общий чат).
|
||||
|
||||
## Подтверждение деструктивных действий
|
||||
|
||||
Блокировка/удаление пользователя, удаление хоста/сервиса/группы и
|
||||
перевыпуск уже выданного ключа (экспорт поверх существующего) — все эти
|
||||
действия либо необратимы, либо мгновенно и широко режут доступ, поэтому
|
||||
подтверждаются не клавишей (`enter`/`y`, которую легко нажать рефлекторно),
|
||||
а вводом заново сгенерированной случайной строки (`internal/ui/confirm.go`,
|
||||
`generateConfirmToken` — 5 латинских букв, дефис, 3 цифры). Неверный ввод
|
||||
просто сбрасывает поле с пояснением и оставляет на этом же экране; выйти без
|
||||
последствий можно только через `esc`. Выдача **нового** ключа пользователю,
|
||||
у которого его ещё не было, ничего не ломает и подтверждения не требует —
|
||||
трение нужно только там, где есть что сломать.
|
||||
|
||||
## Бэкап и аварийное восстановление
|
||||
|
||||
Каталог состояния (`/var/lib/ssh-access-tui/state`) — git-репозиторий.
|
||||
@@ -172,12 +198,11 @@ session" вместо тихой порчи данных. Если реальн
|
||||
|
||||
## Известные ограничения / что дальше
|
||||
|
||||
- Обновление клиентского конфига пользователя при появлении **нового**
|
||||
разрешённого хоста в его группе требует повторного экспорта архива
|
||||
(что заодно ротирует ключ пользователя — так и задумано: ротация ключа
|
||||
время от времени — отдельная хорошая практика, просто в MVP она не
|
||||
отделена от "обновить конфиг"). Удаление хоста из группы вступает в силу
|
||||
немедленно на сервере (permitopen), без участия клиента.
|
||||
- Обновление клиентского конфига пользователя при появлении нового
|
||||
сервиса/хоста в его группе — отдельный ручной шаг («обновить конфиг»,
|
||||
не требует переиздания ключа); права доступа на сервере (`permitopen=`)
|
||||
обновляются немедленно и автоматически в любом случае, независимо от
|
||||
того, обновил ли пользователь свой локальный `config`.
|
||||
- Архивы в `export_dir` не удаляются автоматически после доставки —
|
||||
администратор должен удалить их вручную, когда получатель подтвердил
|
||||
получение.
|
||||
|
||||
Reference in new issue
Block a user