# ТЗ для ИИ-кодера: sshkeeper — консольный менеджер SSH-подключений для Linux ## 1. Назначение проекта Нужно разработать консольное приложение для Linux, которое работает как менеджер SSH-серверов и учётных данных. Приложение **не должно реализовывать собственный SSH-клиент с нуля**. Оно должно использовать системный OpenSSH (`/usr/bin/ssh`) как реальный транспорт подключения, а само приложение должно быть удобным слоем управления над: - списком серверов; - пользователями; - портами; - SSH-ключами; - SSH-паролями; - группами; - тегами; - заметками; - bastion/proxyjump; - локальными настройками подключения; - encrypted vault для секретов. Рабочее название приложения: **sshkeeper**. Главная идея: > `sshkeeper` не заменяет OpenSSH. > `sshkeeper` управляет профилями подключений, секретами и удобным запуском SSH-сессий. --- ## 2. Целевая платформа Основная целевая платформа: - Linux x86_64; - Arch Linux; - Debian/Ubuntu; - Fedora-compatible дистрибутивы. На первом этапе Windows и macOS не обязательны. --- ## 3. Основной стек Использовать: - Go; - Cobra для CLI; - Bubble Tea для TUI; - Bubbles для TUI-компонентов; - SQLite для локальной базы профилей; - `modernc.org/sqlite` как SQLite-драйвер без CGO; - `golang.org/x/crypto/argon2` для Argon2id KDF; - `golang.org/x/crypto/chacha20poly1305` для XChaCha20-Poly1305; - `github.com/creack/pty` для запуска OpenSSH через PTY; - системный `/usr/bin/ssh`. Не использовать: - Electron; - GUI; - внешние password managers как обязательную зависимость; - GNOME Keyring/KWallet/libsecret как обязательную зависимость; - хранение паролей в plaintext; - передачу пароля через аргументы командной строки; - передачу пароля через environment variables. --- ## 4. Основные пользовательские сценарии ### 4.1. Добавить сервер Пользователь запускает: ```bash sshkeeper add ``` Приложение открывает интерактивную форму в терминале. Поля: - Alias; - Display name; - Host; - Port; - User; - Auth method: - `password`; - `key`; - `key+passphrase`; - `agent`; - Identity file, если используется ключ; - Password, если используется пароль; - Key passphrase, если используется ключ с passphrase; - Group; - Tags; - Notes; - ProxyJump, опционально; - Local forwards, опционально; - Remote forwards, опционально. В конце формы должны быть две основные кнопки: ```text [Test] [Save] ``` --- ### 4.2. Кнопка Test Кнопка `Test` проверяет текущие введённые данные. Важно: - `Test` **не сохраняет** профиль. - `Test` **не сохраняет** новые секреты в постоянный vault. - `Test` использует введённые данные только из текущей формы. - Если подключение успешно — показать `Connection OK`. - Если подключение неуспешно — показать ошибку. - После теста пользователь остаётся в форме. - Все введённые значения должны сохраниться в форме. - Пользователь сам решает, нажимать ли потом `Save`. Пример успешного теста: ```text Connection OK. ``` Пример ошибки пароля: ```text Connection failed: Permission denied, please try again. ``` Пример ошибки ключа: ```text Connection failed: Identity file ~/.ssh/prod_ed25519 not found. ``` Пример сетевой ошибки: ```text Connection failed: connect to host 10.0.0.11 port 22: No route to host. ``` --- ### 4.3. Кнопка Save Кнопка `Save` сохраняет профиль как есть. Важно: - `Save` **не обязан** выполнять тест подключения. - `Save` **не должен** блокировать сохранение, если сервер недоступен. - `Save` **не должен** показывать раздражающие предупреждения про опасность SSH-паролей. - Если пользователь выбрал `password`, пароль сохраняется в собственный encrypted vault. - Если пользователь выбрал `key+passphrase`, passphrase сохраняется в encrypted vault. - Обычные данные профиля сохраняются в SQLite. - Секреты не должны попадать в SQLite в открытом виде. После сохранения показать: ```text Saved. ``` --- ## 5. Пароли как штатный режим Парольная авторизация должна быть полноценным штатным режимом. Не нужно делать предупреждения вида: ```text WARNING! Password auth is insecure! ``` Такие предупреждения не нужны. Логика: - `auth=password` — нормальный режим. - Пароль хранится в собственном encrypted vault. - Пароль не хранится в YAML/TOML/SQLite открытым текстом. - Пароль не передаётся в argv. - Пароль не передаётся через env. - Пароль не пишется в логи. - Пароль не показывается в интерфейсе после ввода. - В списке серверов можно показывать тип авторизации: `password`, `key`, `agent`, `key+passphrase`. --- ## 6. Структура хранения данных Использовать XDG-совместимые пути. База данных: ```text ~/.local/share/sshkeeper/sshkeeper.db ``` Vault: ```text ~/.local/share/sshkeeper/vault.bin ``` Конфиг приложения: ```text ~/.config/sshkeeper/config.toml ``` Сгенерированный OpenSSH config: ```text ~/.ssh/config.d/sshkeeper.conf ``` Пользовательский `~/.ssh/config` может содержать: ```sshconfig Include ~/.ssh/config.d/*.conf ``` Если такой строки нет, приложение должно уметь добавить её отдельной командой: ```bash sshkeeper ssh-config install-include ``` Делать это автоматически без явного действия пользователя не нужно. --- ## 7. SQLite-схема MVP Минимальные таблицы: --- ### 7.1. `servers` Поля: - `id`; - `alias`; - `display_name`; - `host`; - `port`; - `user`; - `auth_method`; - `identity_file`; - `proxy_jump`; - `group_name`; - `notes`; - `created_at`; - `updated_at`; - `last_connected_at`; - `last_test_at`; - `last_test_status`; - `last_test_error`. `auth_method` значения: - `password`; - `key`; - `key_passphrase`; - `agent`. `last_test_status` значения: - `unknown`; - `ok`; - `failed`. --- ### 7.2. `tags` Поля: - `id`; - `name`. --- ### 7.3. `server_tags` Поля: - `server_id`; - `tag_id`. --- ### 7.4. `forwards` Поля: - `id`; - `server_id`; - `type`; - `local_addr`; - `local_port`; - `remote_addr`; - `remote_port`. `type` значения: - `local`; - `remote`; - `dynamic`. --- ### 7.5. `command_templates` Поля: - `id`; - `server_id`; - `name`; - `command`. Примеры command templates: - `logs` → `journalctl -xe`; - `docker` → `docker ps`; - `nginx-test` → `nginx -t`. --- ## 8. Vault Нужен собственный encrypted vault. --- ### 8.1. Master password При первом запуске: ```bash sshkeeper init ``` Приложение спрашивает: ```text Create master password: Repeat master password: ``` Master password не хранится. Из master password через Argon2id выводится master key. --- ### 8.2. KDF Использовать Argon2id. Начальные параметры: ```text memory: 64 MiB iterations: 3 parallelism: 1 salt: random 16 или 32 bytes key length: 32 bytes ``` Параметры KDF должны храниться в metadata vault, чтобы в будущем можно было менять настройки. --- ### 8.3. Шифрование Использовать XChaCha20-Poly1305. Каждая запись vault должна иметь отдельный случайный nonce. Формат vault можно сделать JSON или бинарный. Для MVP допустим JSON с base64-полями. Пример структуры: ```json { "version": 1, "kdf": { "name": "argon2id", "memory_kib": 65536, "iterations": 3, "parallelism": 1, "salt": "base64..." }, "records": [ { "id": "server:old-router:ssh-password", "type": "ssh_password", "nonce": "base64...", "ciphertext": "base64..." } ] } ``` --- ### 8.4. Типы секретов Поддержать типы: - `ssh_password`; - `key_passphrase`; - `sudo_password`; - `custom_secret`. Для MVP обязательны: - `ssh_password`; - `key_passphrase`. --- ### 8.5. Secret references В SQLite хранить только ссылки на секреты. Примеры: ```text server:old-router:ssh-password server:prod-web-1:key-passphrase server:prod-web-1:sudo-password ``` Секреты должны лежать только в vault. --- ## 9. Подключение к серверу Команда: ```bash sshkeeper connect ``` Короткий алиас: ```bash sshkeeper c ``` Логика: 1. Найти профиль сервера в SQLite. 2. Если нужен vault — запросить master password, если vault ещё не разблокирован. 3. Сформировать команду `ssh`. 4. Запустить системный `/usr/bin/ssh`. 5. Если используется пароль — запустить SSH через PTY-wrapper. 6. Дождаться password prompt. 7. Отправить пароль в PTY. 8. Передать управление пользователю. 9. После завершения сессии обновить `last_connected_at`. --- ## 10. PTY-wrapper для паролей Для `auth=password` нельзя передавать пароль через аргументы командной строки. Нужен PTY-wrapper. Примерная логика: ```text start ssh through PTY read PTY output detect password prompt write password + "\n" after login, bridge stdin/stdout/stderr between user terminal and PTY handle terminal resize restore terminal state on exit ``` Prompt detection должен учитывать варианты: ```text password: Password: user@host's password: Enter password: ``` Для MVP достаточно английских вариантов. Позже можно добавить расширяемые regex-шаблоны в config. --- ## 11. Проверка подключения Команда: ```bash sshkeeper test ``` В форме сервера кнопка: ```text [Test] ``` Проверка должна выполнять короткую безопасную команду: ```bash echo SSHKEEPER_OK ``` Ожидаемый вывод: ```text SSHKEEPER_OK ``` Если команда выполнилась и вывод получен — тест успешен. Для нестандартных систем можно добавить fallback: ```bash exit ``` Но для MVP достаточно `echo SSHKEEPER_OK`. Важно: - Тест из формы не сохраняет профиль. - Тест существующего профиля обновляет: - `last_test_at`; - `last_test_status`; - `last_test_error`. - Тест должен иметь timeout. - Начальный timeout: 10 секунд. - Timeout должен настраиваться в config. --- ## 12. CLI-команды MVP Обязательные команды: ```bash sshkeeper init sshkeeper add sshkeeper list sshkeeper show sshkeeper edit sshkeeper delete sshkeeper connect sshkeeper c sshkeeper test sshkeeper search sshkeeper vault lock sshkeeper vault unlock sshkeeper vault status sshkeeper vault change-password sshkeeper config path sshkeeper ssh-config generate sshkeeper ssh-config install-include ``` Дополнительные команды, если останется время: ```bash sshkeeper import ~/.ssh/config sshkeeper export sshkeeper run sshkeeper run-template