sshkeeper/sshkeeper_tz.md

1353 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ТЗ для ИИ-кодера: 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 <alias>
```
Короткий алиас:
```bash
sshkeeper c <alias>
```
Логика:
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 <alias>
```
В форме сервера кнопка:
```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 <alias>
sshkeeper edit <alias>
sshkeeper delete <alias>
sshkeeper connect <alias>
sshkeeper c <alias>
sshkeeper test <alias>
sshkeeper search <query>
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 <alias> <command>
sshkeeper run-template <alias> <template>
sshkeeper group list
sshkeeper group test <group>
```
---
## 13. TUI MVP
При запуске без аргументов:
```bash
sshkeeper
```
Открывается TUI.
Главный экран:
```text
┌─ sshkeeper ─────────────────────────────────────────────┐
│ Search: _ │
├──────────────────────────────────────────────────────────┤
│ [?] home-nextcloud admin@192.168.1.15:22 key │
│ [✓] prod-web-1 root@10.0.0.11:22 key │
│ [!] old-router admin@192.168.1.1:22 password │
└──────────────────────────────────────────────────────────┘
Enter connect | a add | e edit | d delete | t test | / search | q quit
```
Статусы:
```text
[?] never tested
[✓] last test OK
[!] last test failed
```
Клавиши:
- `Enter` — connect;
- `a` — add;
- `e` — edit;
- `d` — delete;
- `t` — test;
- `/` — search;
- `q` — quit;
- `Esc` — назад/отмена.
Форма добавления/редактирования должна иметь две основные кнопки:
```text
[Test] [Save]
```
`Test` проверяет, но не сохраняет.
`Save` сохраняет без обязательного теста.
---
## 14. Генерация OpenSSH config
Команда:
```bash
sshkeeper ssh-config generate
```
Должна создавать файл:
```text
~/.ssh/config.d/sshkeeper.conf
```
Для серверов с ключами можно генерировать:
```sshconfig
Host prod-web-1
HostName 10.0.0.11
User root
Port 22
IdentityFile ~/.ssh/prod_ed25519
ProxyJump bastion
```
Для password-auth профилей тоже можно генерировать базовый host без пароля:
```sshconfig
Host old-router
HostName 192.168.1.1
User admin
Port 22
```
Пароли в ssh config не писать никогда.
---
## 15. Логи
Логи должны быть аккуратными.
Не логировать:
- SSH-пароли;
- key passphrase;
- master password;
- decrypted vault content.
Можно логировать:
- alias;
- host;
- port;
- user;
- auth method;
- ошибку подключения без секретов.
Логи MVP можно писать только в stderr/debug mode.
---
## 16. Безопасность файлов
При создании файлов выставлять права:
```text
~/.local/share/sshkeeper/sshkeeper.db 0600
~/.local/share/sshkeeper/vault.bin 0600
~/.config/sshkeeper/config.toml 0600
~/.ssh/config.d/sshkeeper.conf 0600
```
Директории:
```text
~/.local/share/sshkeeper 0700
~/.config/sshkeeper 0700
~/.ssh/config.d 0700 или существующие безопасные права
```
---
## 17. Конфиг приложения
Файл:
```text
~/.config/sshkeeper/config.toml
```
Пример:
```toml
[ssh]
binary = "/usr/bin/ssh"
connect_timeout_seconds = 10
test_command = "echo SSHKEEPER_OK"
[vault]
auto_lock_minutes = 15
[ui]
show_security_hints = false
```
---
## 18. Архитектура кода
Предлагаемая структура проекта:
```text
sshkeeper/
go.mod
go.sum
main.go
cmd/
root.go
init.go
add.go
list.go
show.go
edit.go
delete.go
connect.go
test.go
search.go
vault.go
ssh_config.go
internal/
app/
app.go
config/
config.go
paths.go
db/
db.go
migrations.go
servers.go
tags.go
forwards.go
model/
server.go
secret.go
forward.go
tag.go
vault/
vault.go
crypto.go
format.go
unlock.go
ssh/
command.go
launcher.go
pty.go
test.go
configgen.go
tui/
app.go
list.go
form.go
styles.go
prompt/
input.go
password.go
util/
fs.go
time.go
errors.go
```
---
## 19. Ошибки и UX
Ошибки должны быть понятными человеку.
Плохо:
```text
exit status 255
```
Хорошо:
```text
Connection failed:
Permission denied.
Server: old-router
Target: admin@192.168.1.1:22
Auth: password
```
При этом не надо добавлять длинные лекции и предупреждения.
---
## 20. Этапы разработки
### Этап 1. Каркас CLI
Сделать:
- Go module;
- Cobra root command;
- команды:
- `init`;
- `add`;
- `list`;
- `show`;
- `delete`;
- XDG paths;
- создание директорий;
- базовый config.
Критерий готовности:
```bash
sshkeeper init
sshkeeper add
sshkeeper list
sshkeeper show test-server
```
Команды работают.
---
### Этап 2. SQLite
Сделать:
- SQLite подключение;
- migrations;
- таблицу `servers`;
- CRUD серверов;
- хранение tags/groups/notes можно пока упростить.
Критерий готовности:
- серверы сохраняются между запусками;
- можно добавить, посмотреть, удалить сервер.
---
### Этап 3. Vault
Сделать:
- `vault.bin`;
- master password;
- Argon2id key derivation;
- XChaCha20-Poly1305 encryption;
- команды:
- `vault unlock`;
- `vault lock`;
- `vault status`;
- `vault change-password`;
- сохранение `ssh_password`;
- сохранение `key_passphrase`.
Критерий готовности:
- пароль можно сохранить;
- пароль не виден в SQLite;
- после перезапуска пароль можно достать только через master password.
---
### Этап 4. Connect через OpenSSH
Сделать:
- `sshkeeper connect <alias>`;
- `sshkeeper c <alias>`;
- запуск `/usr/bin/ssh`;
- key auth;
- agent auth;
- password auth через PTY-wrapper.
Критерий готовности:
- можно подключиться к серверу по ключу;
- можно подключиться к серверу по паролю;
- пароль не передаётся через argv/env.
---
### Этап 5. Test
Сделать:
- `sshkeeper test <alias>`;
- test command: `echo SSHKEEPER_OK`;
- timeout;
- обновление:
- `last_test_at`;
- `last_test_status`;
- `last_test_error`.
Критерий готовности:
- успешное подключение отмечается `[✓]`;
- неуспешное — `[!]`;
- ошибка сохраняется и показывается.
---
### Этап 6. TUI
Сделать:
- запуск TUI при `sshkeeper`;
- список серверов;
- поиск;
- connect;
- add form;
- edit form;
- delete;
- test;
- кнопки `[Test] [Save]`.
Критерий готовности:
- приложением можно пользоваться без знания CLI-команд;
- добавление/редактирование сервера возможно из TUI;
- `Test` не сохраняет;
- `Save` сохраняет без обязательного теста.
---
### Этап 7. OpenSSH config generation
Сделать:
- `sshkeeper ssh-config generate`;
- генерация `~/.ssh/config.d/sshkeeper.conf`;
- `sshkeeper ssh-config install-include`;
- не писать пароли в ssh config.
Критерий готовности:
- после генерации можно выполнить:
```bash
ssh alias
```
для key/agent/password профилей, где password всё равно будет спрашиваться самим ssh.
---
### Этап 8. Полировка
Сделать:
- импорт из `~/.ssh/config`;
- export/backup;
- groups;
- command templates;
- run command;
- group test;
- fuzzy search;
- нормальные help-сообщения.
---
## 21. Минимальный acceptance checklist
Приложение считается MVP-готовым, если:
- `sshkeeper init` создаёт конфиг, БД и vault.
- `sshkeeper add` добавляет сервер.
- `sshkeeper list` показывает серверы.
- `sshkeeper show <alias>` показывает профиль без секретов.
- `sshkeeper edit <alias>` редактирует профиль.
- `sshkeeper delete <alias>` удаляет профиль.
- `sshkeeper c <alias>` подключается через OpenSSH.
- password-auth работает через PTY.
- key-auth работает.
- key+passphrase работает.
- пароль не хранится в plaintext.
- пароль не передаётся через argv/env.
- `sshkeeper test <alias>` проверяет подключение.
- TUI запускается командой `sshkeeper`.
- В TUI есть список серверов.
- В TUI есть форма add/edit.
- В форме есть кнопки `Test` и `Save`.
- `Test` не сохраняет.
- `Save` сохраняет без обязательного теста.
- `sshkeeper ssh-config generate` создаёт OpenSSH config без паролей.
---
# Инструкция по настройке среды разработки, сборке и запуску
## 1. Установить зависимости ОС
### Arch Linux
```bash
sudo pacman -Syu
sudo pacman -S go git openssh make
```
### Debian/Ubuntu
```bash
sudo apt update
sudo apt install -y golang-go git openssh-client make
```
Если нужна свежая версия Go, лучше установить Go с официального сайта, а не из репозитория дистрибутива.
Проверить:
```bash
go version
git --version
ssh -V
```
---
## 2. Создать проект
```bash
mkdir -p ~/projects
cd ~/projects
mkdir sshkeeper
cd sshkeeper
go mod init github.com/mirivlad/sshkeeper
```
---
## 3. Установить зависимости Go
```bash
go get github.com/spf13/cobra@latest
go get github.com/charmbracelet/bubbletea@latest
go get github.com/charmbracelet/bubbles@latest
go get github.com/charmbracelet/lipgloss@latest
go get modernc.org/sqlite@latest
go get golang.org/x/crypto@latest
go get github.com/creack/pty@latest
```
---
## 4. Создать минимальный `main.go`
```go
package main
import "github.com/mirivlad/sshkeeper/cmd"
func main() {
cmd.Execute()
}
```
---
## 5. Создать `Makefile`
```makefile
APP=sshkeeper
.PHONY: build run test clean install
build:
go build -o bin/$(APP) .
run:
go run .
test:
go test ./...
clean:
rm -rf bin
install:
go build -o $(HOME)/.local/bin/$(APP) .
```
---
## 6. Собрать приложение
```bash
make build
```
Проверить:
```bash
./bin/sshkeeper --help
```
---
## 7. Запустить из исходников
```bash
go run . --help
go run . init
go run . list
```
---
## 8. Установить локально в PATH
Убедиться, что есть директория:
```bash
mkdir -p ~/.local/bin
```
Добавить в `~/.bashrc` или `~/.zshrc`, если ещё не добавлено:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
Применить:
```bash
source ~/.bashrc
```
или:
```bash
source ~/.zshrc
```
Установить:
```bash
make install
```
Проверить:
```bash
sshkeeper --help
```
---
## 9. Первый запуск
```bash
sshkeeper init
```
Ожидаемый результат:
```text
Created config: ~/.config/sshkeeper/config.toml
Created database: ~/.local/share/sshkeeper/sshkeeper.db
Created vault: ~/.local/share/sshkeeper/vault.bin
```
---
## 10. Добавить тестовый сервер
Интерактивно:
```bash
sshkeeper add
```
Или позже можно реализовать неинтерактивный вариант:
```bash
sshkeeper add test-vps \
--host 192.168.1.10 \
--port 22 \
--user root \
--auth key \
--identity-file ~/.ssh/id_ed25519
```
---
## 11. Проверить подключение
```bash
sshkeeper test test-vps
```
---
## 12. Подключиться
```bash
sshkeeper c test-vps
```
---
## 13. Запустить TUI
```bash
sshkeeper
```
---
## 14. Сгенерировать OpenSSH config
```bash
sshkeeper ssh-config generate
```
При необходимости добавить Include:
```bash
sshkeeper ssh-config install-include
```
После этого для части профилей можно будет подключаться напрямую:
```bash
ssh test-vps
```
---
## 15. Рекомендации к разработке
После каждого этапа запускать:
```bash
go fmt ./...
go vet ./...
go test ./...
make build
```
Если есть ошибки — исправить до перехода к следующему этапу.
Не переходить к TUI, пока CLI, SQLite, vault и connect не работают стабильно.
---
# Примечания по философии проекта
`sshkeeper` должен оставаться слоем управления, а не пытаться стать новым OpenSSH.
OpenSSH уже умеет:
- `Host`;
- `HostName`;
- `User`;
- `Port`;
- `IdentityFile`;
- `ProxyJump`;
- `LocalForward`;
- `RemoteForward`;
- `Include`.
Поэтому `sshkeeper` должен:
- использовать OpenSSH как транспорт;
- генерировать совместимый config;
- хранить секреты отдельно;
- давать удобный CLI/TUI;
- не спорить с пользователем;
- помогать, но не блокировать.
Главное UX-правило:
> `Test` проверяет.
> `Save` сохраняет.
> Пользователь сам решает, что ему нужно.