12 KiB
Архитектура и модель угроз
Почему TUI, а не веб
Веб-панель для управления пользователями требует бэкенда с root-правами
(создание Linux-аккаунтов, правка authorized_keys, sshd_config),
слушающего порт. Это новый привилегированный сетевой процесс на хосте,
который и так является входной точкой для множества людей: своя
аутентификация/сессии/CSRF, TLS-сертификаты, обновления фреймворка — то есть
дополнительная поверхность атаки, не связанная с самой задачей.
TUI работает поверх уже существующего SSH+sudo-доступа администратора к
этому хосту — доступа, который и так есть и уже находится под контролем
(ключи, MFA, аудит доступа к самому jump-хосту). Новых портов, новых
механизмов аутентификации и нового ПО, которое надо патчить, не появляется.
Цена этого решения: нет привычного браузерного UX, и параллельная работа нескольких админов не даётся бесплатно фреймворком — её пришлось реализовать вручную (см. «Конкурентность» ниже). Для небольшой команды администраторов это разумный компромисс.
Как на самом деле работает ограничение "только прыжок/форвардинг"
Это два независимых слоя, и путать их нельзя:
-
Сервер (реальная граница безопасности). Каждому ключу в
authorized_keysпользователя приписаны опции:restrict,port-forwarding,permitopen="10.0.1.5:5432",permitopen="10.0.2.9:443" ssh-ed25519 AAAA... user@jumprestrictразом отключает 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 на случай, если опции конкретного ключа окажутся повреждены/потеряны, а не основной механизм. -
Клиент (только удобство). Файл
~/.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не удаляются автоматически после доставки — администратор должен удалить их вручную, когда получатель подтвердил получение. - Полностью параллельная работа нескольких админов не поддерживается (см. «Конкурентность»).