v0.2.0
This commit is contained in:
1 parent
23c7133fa9
commit
8c1b93c14e
16 files changed
+1187
-295
No files matched your search
+97
-39
@@ -16,7 +16,16 @@ sudo ssh-access-tui
|
||||
перезапишет чужие изменения.
|
||||
|
||||
Навигация везде: стрелки/`j`/`k` — перемещение по списку, `enter` —
|
||||
выбрать/подтвердить, `esc` — назад, `ctrl+c` — выход в любой момент.
|
||||
выбрать/подтвердить, `esc` — назад, `ctrl+c` — жёсткий выход в любой момент
|
||||
(без каких-либо предупреждений — только для аварийных случаев). Обычный
|
||||
выход через пункт меню «Выход» или `q` в главном меню предупредит, если
|
||||
есть созданные, но ни разу не экспортированные пользователи (у них ещё нет
|
||||
ключа) — можно выйти всё равно (`enter`/`y`) или вернуться (`esc`/`n`).
|
||||
|
||||
Любое деструктивное действие (блокировка/удаление пользователя, удаление
|
||||
хоста или сервиса) сначала показывает отдельный экран подтверждения с
|
||||
пояснением последствий — ничего не происходит до `enter`/`y`; `esc`/`n`
|
||||
отменяет без каких-либо изменений.
|
||||
|
||||
## Главное меню
|
||||
|
||||
@@ -28,74 +37,121 @@ sudo ssh-access-tui
|
||||
Выход
|
||||
```
|
||||
|
||||
Имена групп, хостов и сервисов должны быть без пробелов (латиница, цифры,
|
||||
`. _ -`) — они попадают в конфиг и в `authorized_keys`; инструмент откажет с
|
||||
понятной ошибкой, если ввести имя с пробелом.
|
||||
|
||||
## Сценарий 1: новый сотрудник, у которого уже есть роль/группа доступа
|
||||
|
||||
Обычно сначала настраивают группу (один раз), потом заводят пользователей в
|
||||
неё пачками.
|
||||
неё пачками. Иерархия: **группа** → **хосты** → **сервисы (порты) на хосте**
|
||||
— на одном хосте может быть несколько сервисов (например, `ssh` на 22 и
|
||||
`web-api` на 7888), каждый добавляется отдельно.
|
||||
|
||||
1. **Группы и хосты** → `n` → ввести имя группы (например `team-billing`) →
|
||||
`enter`.
|
||||
2. Выбрать группу → `enter`, попасть в список хостов группы (изначально
|
||||
пуст).
|
||||
3. `n` → заполнить: имя хоста (короткий алиас, например `db-prod-1`), адрес
|
||||
(как виден с jump-хоста), порт, описание (необязательно) → `tab` между
|
||||
полями → `enter` сохранить. Повторить для каждого внутреннего хоста,
|
||||
который должен быть доступен этой группе.
|
||||
4. `esc` дважды — назад в главное меню.
|
||||
5. **Пользователи** → `n` → имя пользователя (латиницей, будущий Linux-логин)
|
||||
3. `n` → заполнить: имя хоста (короткое, например `db-prod-1`), адрес (как
|
||||
виден с jump-хоста) → `tab` между полями → `enter`. Сразу после этого
|
||||
попадаете в список сервисов этого хоста (тоже изначально пуст).
|
||||
4. `n` → заполнить сервис: имя (например `ssh` или `web-api`), порт на
|
||||
хосте (например `22` или `7888`), описание (необязательно) → `enter`.
|
||||
Повторите `n` столько раз, сколько сервисов есть на этом хосте.
|
||||
5. `esc` — назад к списку хостов группы; повторите шаги 3-4 для остальных
|
||||
хостов группы, затем `esc` дважды — назад в главное меню.
|
||||
6. **Пользователи** → `n` → имя пользователя (латиницей, будущий Linux-логин)
|
||||
и комментарий (например ФИО) → `enter`.
|
||||
6. Отметить `space` нужные группы (можно несколько — эффективный список
|
||||
хостов пользователя = объединение хостов всех отмеченных групп) → `enter`.
|
||||
7. Отметить `space` нужные группы (можно несколько — эффективный доступ
|
||||
пользователя = объединение сервисов всех хостов всех отмеченных групп) →
|
||||
`enter`.
|
||||
|
||||
Это уже создаёт системный Linux-аккаунт (без пароля, без интерактивного
|
||||
шелла) и запись в состоянии. **Ключ ещё не выдан** — статус в списке
|
||||
пользователей покажет «ключ не выдан».
|
||||
|
||||
7. **Экспорт архивов** → отметить `space` только что созданного пользователя
|
||||
(или `a` — отметить всех, удобно при пакетном найме) → `enter`.
|
||||
8. Инструмент сгенерирует ключ, пропишет `permitopen=` под текущие хосты
|
||||
группы, соберёт `.7z` и покажет **один общий пароль на всю партию** — он
|
||||
8. **Экспорт архивов** → отметить `space` только что созданного пользователя
|
||||
(или `a` — отметить всех, удобно при пакетном найме) → `enter`. Список
|
||||
сразу показывает у каждого пользователя, будет ли выдан новый ключ или
|
||||
существующий будет **перевыпущен** (актуально, если случайно попадёт в
|
||||
выборку уже работающий пользователь).
|
||||
9. Экран подтверждения: для тех, у кого ключ уже был, явно предупредит, что
|
||||
старый архив перестанет работать — только после `enter`/`y` (либо `esc`/`n`
|
||||
для отмены, тогда не создаётся и не меняется вообще ничего) инструмент
|
||||
сгенерирует ключ, пропишет `permitopen=` под текущие сервисы группы и
|
||||
соберёт `.7z`, показав **один общий пароль на всю партию** — он
|
||||
показывается один раз, запишите/скопируйте сразу.
|
||||
9. Передайте каждому пользователю его архив (файл, путь показан на экране,
|
||||
лежит в `export_dir`) любым каналом, а пароль от архива — **отдельным**
|
||||
каналом (голосом, в личку) и по возможности не всем одним и тем же
|
||||
сообщением в общем чате.
|
||||
10. Пользователь распаковывает архив на Windows по инструкции
|
||||
`INSTRUCTIONS.txt` внутри архива (она уже сгенерирована под его
|
||||
конкретные разрешённые хосты).
|
||||
10. Передайте каждому пользователю его архив (файл, путь показан на экране,
|
||||
лежит в `export_dir`) любым каналом, а пароль от архива — **отдельным**
|
||||
каналом (голосом, в личку) и по возможности не всем одним и тем же
|
||||
сообщением в общем чате.
|
||||
11. Пользователь распаковывает архив на Windows по инструкции
|
||||
`INSTRUCTIONS.txt` внутри архива — она уже на простом русском языке,
|
||||
без технических терминов: просто список "локальный адрес → какой это
|
||||
сервис → куда он реально ведёт", составленный под конкретные разрешённые
|
||||
сервисы этого пользователя.
|
||||
|
||||
## Сценарий 2: добавили новый разрешённый хост существующей группе
|
||||
## Сценарий 1а: изменить группы уже существующего пользователя
|
||||
|
||||
**Пользователи** → выбрать пользователя → `g` → отметить/снять `space`
|
||||
нужные группы → `enter`. Если у пользователя уже выдан ключ, инструмент
|
||||
сразу пересоберёт его `permitopen=` под новый набор сервисов — доступ
|
||||
меняется немедленно, без переотправки архива и **без замены ключа**
|
||||
(изменение групп никогда не ротирует ключ).
|
||||
|
||||
Если хотите, чтобы у пользователя появился ещё и обновлённый клиентский
|
||||
`config` с новыми `LocalForward` для новых сервисов (иначе он про них
|
||||
просто не узнает, хотя доступ уже есть) — **Пользователи** → выбрать
|
||||
пользователя → `c` («обновить конфиг»). Это генерирует маленький архив
|
||||
только с `config` + `INSTRUCTIONS.txt` (без ключа — ключ у пользователя уже
|
||||
есть и остаётся тем же), пользователю нужно только подменить файл `config`.
|
||||
Не требует подтверждения — ключ и доступ этим действием не меняются, это не
|
||||
деструктивно.
|
||||
|
||||
Полный экспорт (со сценария 1, шаги 8-9) тоже сработает и даст то же самое
|
||||
плюс заново выпустит ключ — используйте `c`, когда переиздание ключа не
|
||||
нужно (обычный случай при простом изменении групп).
|
||||
|
||||
## Сценарий 2: добавили новый сервис/хост существующей группе
|
||||
|
||||
1. **Группы и хосты** → выбрать группу → `enter`.
|
||||
2. `n` → заполнить новый хост → `enter`.
|
||||
2. Либо `n` — новый хост (затем добавить ему хотя бы один сервис), либо
|
||||
выбрать существующий хост → `enter` → `n` — новый сервис на нём.
|
||||
|
||||
Это сразу обновляет `permitopen=` в `authorized_keys` **всех активных
|
||||
пользователей** этой группы — доступ появляется немедленно, без каких-либо
|
||||
действий на стороне пользователя. Клиентский конфиг пользователя (его
|
||||
`~/.ssh/config`) при этом не обновляется автоматически — если хотите, чтобы
|
||||
у пользователя появился готовый алиас для нового хоста, а не только "голый"
|
||||
доступ, повторно прогоните его через **Экспорт архивов** (это заодно
|
||||
ротирует его ключ — считайте это плановой ротацией, не проблемой).
|
||||
у пользователя появилась готовая строка `LocalForward` для нового сервиса, а
|
||||
не только "голый" доступ на сервере, используйте **Пользователи** → `c`
|
||||
(«обновить конфиг») — ключ при этом не меняется (см. сценарий 1а).
|
||||
|
||||
Удаление хоста из группы (`d` в списке хостов) действует так же немедленно
|
||||
и в обратную сторону.
|
||||
Удаление сервиса или целого хоста (`d` в соответствующем списке) — оба
|
||||
действия **деструктивны** (мгновенно отбирают доступ у всех пользователей
|
||||
группы) и требуют подтверждения: экран покажет, что именно удаляется и
|
||||
скольких пользователей это коснётся, `enter`/`y` подтверждает, `esc`/`n`
|
||||
отменяет без каких-либо изменений.
|
||||
|
||||
## Сценарий 3: сотрудник уволился
|
||||
|
||||
1. **Пользователи** → выбрать пользователя → `l` (заблокировать).
|
||||
1. **Пользователи** → выбрать пользователя → `l` (заблокировать) → экран
|
||||
подтверждения объяснит, что произойдёт → `enter`/`y`.
|
||||
|
||||
Это одновременно: блокирует пароль аккаунта, устанавливает немедленный срок
|
||||
истечения аккаунта (`chage -E 1`) и комментирует его ключ в
|
||||
Блокировка — это одновременно: блокирует пароль аккаунта, устанавливает
|
||||
немедленный срок истечения аккаунта (`chage -E 1`) и комментирует его ключ в
|
||||
`authorized_keys` — три независимых механизма, чтобы блокировка не зависела
|
||||
от одного из них. Статус в списке сменится на `locked`.
|
||||
от одного из них. Статус в списке сменится на `locked`. Это **обратимо**.
|
||||
|
||||
Если сотрудник вернулся — `u` (разблокировать) на том же пользователе:
|
||||
снимает блокировку аккаунта и восстанавливает его ключ в `authorized_keys`
|
||||
с текущими правами групп.
|
||||
с текущими правами групп. Разблокировка не деструктивна — подтверждения не
|
||||
требует, срабатывает сразу.
|
||||
|
||||
Полное удаление аккаунта (`d`) — необратимо (`userdel -r`, домашняя папка
|
||||
стирается), используйте для окончательного оффбординга, не для временной
|
||||
блокировки.
|
||||
Полное удаление аккаунта (`d`) — **необратимо** (`userdel -r`, домашняя папка
|
||||
стирается) и тоже требует подтверждения (экран явно предупредит, что это
|
||||
необратимо и что для временного отключения лучше использовать блокировку).
|
||||
Используйте удаление только для окончательного оффбординга, не для
|
||||
временной блокировки.
|
||||
|
||||
## Сценарий 4: проверить, кто сейчас подключён / посмотреть историю
|
||||
|
||||
@@ -125,9 +181,11 @@ Linux-аккаунты для активных пользователей, вы
|
||||
## Что не делает эта версия (нужно руками)
|
||||
|
||||
- Не удаляет `.7z` из `export_dir` после доставки — сделайте это сами,
|
||||
когда получатель подтвердил, что распаковал архив.
|
||||
когда получатель подтвердил, что распаковал архив (это касается и
|
||||
архивов от «обновить конфиг», хоть они и не содержат ключ).
|
||||
- Не обновляет клиентский `~/.ssh/config` пользователя автоматически при
|
||||
появлении нового хоста в его группе — только права доступа на сервере
|
||||
обновляются немедленно (см. сценарий 2).
|
||||
появлении нового сервиса/хоста в его группе — это ручной шаг («обновить
|
||||
конфиг», сценарий 1а), права доступа на сервере при этом обновляются
|
||||
немедленно и без участия администратора (см. сценарий 2).
|
||||
- Не поддерживает одновременную работу нескольких администраторов —
|
||||
рассчитан на одного за раз (см. «Конкурентность» в ARCHITECTURE.md).
|
||||
+49
-12
@@ -19,6 +19,24 @@ TUI работает поверх уже существующего SSH+`sudo`-
|
||||
реализовать вручную (см. «Конкурентность» ниже). Для небольшой команды
|
||||
администраторов это разумный компромисс.
|
||||
|
||||
## Модель доступа: Host → Service → Forward
|
||||
|
||||
Хост (`Host`) — это адрес (например, `172.16.1.20`). На одном хосте может
|
||||
быть несколько сервисов (`Service`) — независимых TCP-портов: `ssh` на 22,
|
||||
какое-то приложение на 7888, и так далее. Группа (`Group`) — это набор
|
||||
хостов (со всеми их сервисами), пользователь может состоять в нескольких
|
||||
группах; его реальный эффективный доступ — объединение сервисов всех хостов
|
||||
всех его групп, это и называется `Forward` (`internal/model/types.go`,
|
||||
`State.AllowedForwards`).
|
||||
|
||||
Ключевой момент: пользователь **никогда не подключается по SSH к целевому
|
||||
хосту напрямую** (ни через `ProxyJump`, ни как-то ещё) — он открывает одно
|
||||
соединение к самому jump-хосту и локально пробрасывает (`LocalForward`)
|
||||
порты к каждому нужному сервису через это соединение. Не важно, слушает ли
|
||||
на целевом порту настоящий SSH (22) или произвольный TCP-сервис (7888) —
|
||||
для форвардинга это не имеет значения, оба варианта — это просто
|
||||
"локальный порт → host:port через jump".
|
||||
|
||||
## Как на самом деле работает ограничение "только прыжок/форвардинг"
|
||||
|
||||
Это два независимых слоя, и путать их нельзя:
|
||||
@@ -27,18 +45,20 @@ TUI работает поверх уже существующего SSH+`sudo`-
|
||||
`authorized_keys` пользователя приписаны опции:
|
||||
|
||||
```
|
||||
restrict,port-forwarding,permitopen="10.0.1.5:5432",permitopen="10.0.2.9:443" ssh-ed25519 AAAA... user@jump
|
||||
restrict,port-forwarding,permitopen="172.16.1.20:22",permitopen="172.16.1.20:7888",permitopen="172.16.1.21:22" 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=`.
|
||||
Один `permitopen=` на каждый `Forward` пользователя (`internal/sysadm/authorizedkeys.go`,
|
||||
`BuildKeyOptions`) — не важно, сколько сервисов на одном хосте или сколько
|
||||
разных хостов используют один и тот же номер порта, каждая пара
|
||||
`host:port` разрешается независимо. `restrict` разом отключает pty,
|
||||
X11-forwarding, agent-forwarding, user-rc и port-forwarding;
|
||||
`port-forwarding` точечно возвращает именно форвардинг (не интерактивный
|
||||
доступ). Это гарантия на уровне протокола 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`,
|
||||
@@ -47,11 +67,12 @@ TUI работает поверх уже существующего SSH+`sudo`-
|
||||
повреждены/потеряны, а не основной механизм.
|
||||
|
||||
2. **Клиент (только удобство).** Файл `~/.ssh/config`, который уезжает
|
||||
пользователю в архиве, — это алиасы (`Host db-prod-1` → `ProxyJump jump`).
|
||||
пользователю в архиве, — это один блок `Host jump` с набором строк
|
||||
`LocalForward <локальный_порт> <host>:<port>` (`internal/keymgr/clientconfig.go`).
|
||||
Он ничего не запрещает: пользователь может отредактировать его или
|
||||
вызвать `ssh` руками с любым хостом. Единственная причина, по которой это
|
||||
не работает, — сервер отклонит `permitopen`, которого нет в списке.
|
||||
Список хостов в клиентском конфиге должен генерироваться из того же
|
||||
Список форвардов в клиентском конфиге должен генерироваться из того же
|
||||
источника правды (группы в `state.yaml`), что и `permitopen=`, — иначе
|
||||
конфиг и реальные права разойдутся.
|
||||
|
||||
@@ -60,6 +81,22 @@ Reverse/remote forwarding (`-R`) в этой модели не выдаётся
|
||||
решению при проектировании: этот джамп-хост используется только для входа
|
||||
внутрь сети, не для проброса портов наружу из неё.
|
||||
|
||||
## Коллизии локальных портов
|
||||
|
||||
Пользователю нужно пробросить сразу много сервисов за одно соединение
|
||||
(например, 10 хостов, на каждом порт 22 и порт 7888 — 20 форвардов сразу).
|
||||
Локально на его машине два разных удалённых назначения не могут занимать
|
||||
один и тот же локальный порт одновременно, а несколько хостов с одинаковым
|
||||
портом (22 или 7888 у всех десяти) — обычная ситуация. Поэтому локальный
|
||||
порт **не всегда равен удалённому**: `internal/keymgr/portalloc.go`
|
||||
(`AllocateLocalPorts`) для каждого форварда сначала пробует занять локальный
|
||||
порт, совпадающий с удалённым (удобно и предсказуемо, когда порт встречается
|
||||
один раз), а при конфликте — берёт следующий свободный из диапазона `20000+`.
|
||||
Присвоение детерминировано (зависит только от порядка и состава форвардов
|
||||
пользователя), поэтому повторный экспорт архива с тем же набором хостов даёт
|
||||
те же локальные порты. Итоговое соответствие "локальный порт → сервис"
|
||||
попадает в `INSTRUCTIONS.txt` внутри архива пользователя.
|
||||
|
||||
## Жизненный цикл приватного ключа
|
||||
|
||||
Приватный ключ **никогда не хранится на диске дольше одной операции
|
||||
|
||||
Reference in new issue
Block a user