Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
1. 下载对应平台的压缩包
2. 解压到任意目录
3. 将 `config.toml.example` 复制为 `config.toml` 并配置
4. 运行 `sshwarden daemon` 启动守护进程
4. 运行 `sshwarden run --background` 启动后台代理,然后运行 `sshwarden login` 登录

详细文档请参阅 [README.md](https://github.com/Hureru/SSHWarden/blob/main/README.md)

Expand Down
6 changes: 3 additions & 3 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ cd SSHWarden
cargo build

# 运行(开发模式)
cargo run -- daemon
cargo run -- run

# 运行特定命令
cargo run -- status
cargo run -- lock
cargo run -- unlock --hello
cargo run -- unlock --method hello
```

### 3. Release 构建
Expand Down Expand Up @@ -347,7 +347,7 @@ brew install openssl
```bash
# 生成性能分析数据
cargo build --release --profile release-with-debug
perf record -g target/release/sshwarden daemon
perf record -g target/release/sshwarden run
perf report

# 或使用 flamegraph
Expand Down
90 changes: 43 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ SSHWarden 的目标不是复刻完整 Bitwarden Desktop,而是提供一个专
| IPC 控制命令 | 已实现;Windows Named Pipe 与 Unix socket 均含调用方鉴权 |
| Local Key Cache | 已实现信封加密模型(格式 v3,PIN 随机盐) |
| 标准平台存储目录 | 已实现,默认平台标准目录(`%APPDATA%`/`$XDG_CONFIG_HOME`/`~/Library/Application Support`);便携模式可选 |
| 自启动 | Windows 已实现;macOS/Linux 待实现 |
| 自启动 | `startup enable`/`disable`;Windows/Linux/macOS 均已实现(Linux/macOS 待实机验证) |
| Shell integration | `sshwarden env` 已实现(sh/fish/powershell/cmd) |
| macOS native unlock | 已实现(Phase 6,Keychain;仍需平台实机验证) |
| Linux native unlock | 已实现(Phase 6,Secret Service;仍需平台实机验证) |
Expand Down Expand Up @@ -92,7 +92,7 @@ SSHWarden 的一等支持平台目标是:
- Signing Request 授权对话框
- Lock / Unlock / Forget
- Local Key Cache
- Control Channel:`status`、`lock`、`unlock`、`sync`、`set-pin` 等控制命令
- Control Channel:`status`、`lock`、`unlock`、`sync`、`set-pin`、`stop` 等控制命令
- Shell Integration:`sshwarden env`
- Startup Integration:登录桌面会话后自动启动
- `status` 简洁状态报告
Expand All @@ -106,8 +106,6 @@ SSHWarden 的一等支持平台目标是:

## 当前使用方式

> 以下命令反映当前实现,后续 CLI 会随 baseline 设计调整。

### 配置

复制配置示例:
Expand All @@ -118,76 +116,74 @@ cp config.toml.example config.toml

编辑 `config.toml`,填写 Bitwarden 邮箱、服务器地址等配置。

### 启动 daemon
### ① 运行 agent(进程生命周期)

```bash
sshwarden daemon
sshwarden run # 前台运行(Ctrl-C 停止);直接运行 `sshwarden` 等价于此
sshwarden run --background # 后台运行
sshwarden stop # 停止后台 agent
sshwarden restart # 重启后台 agent
sshwarden status # 查看 agent / vault 状态与下一步建议
sshwarden doctor [--fix] # 诊断(--fix 执行安全修复,如写入 ~/.ssh/config 的 Include 行)
```

或直接运行:
### ② Vault 会话(需联网)

```bash
sshwarden
sshwarden login # 登录 Bitwarden、同步、设置本机 PIN(Remembered Device),并把 key 交给运行中的 agent
sshwarden sync # 从 Bitwarden 同步 key 到运行中的 agent
sshwarden forget # 删除本机记住的 key/session/PIN
```

### 登录 / 同步 keys
### ③ 锁定状态(不联网)

```bash
sshwarden login
sshwarden keys
sshwarden sync
```

### 绑定 Host / 避免 MaxAuthTries

当 Bitwarden 里有多把 SSH Key 时,OpenSSH 可能会因为 `MaxAuthTries` 在尝试完所有 agent key 前断开连接。SSHWarden 支持把 vault key 绑定到 SSH Host,并生成公开 `.pub` selector 文件与托管 SSH config:

```bash
# 一次性安装 ~/.ssh/config 的 SSHWarden Include
sshwarden ssh-config install

# 将 key 绑定到 host,可使用 key 名称或 vault item id
sshwarden bindings add github-key github.com

# 查看当前状态和生成的配置
sshwarden ssh-config status
sshwarden ssh-config show
sshwarden lock
sshwarden unlock # 默认 auto:先平台解锁,再降级到 PIN
sshwarden unlock --method pin
sshwarden unlock --method hello # Windows
sshwarden unlock --method native # macOS Keychain / Linux Secret Service
sshwarden set-pin
```

也可以通过签名请求里的 **Bind & Approve...** 图形流程完成绑定。详见 [`docs/host-bindings.md`](docs/host-bindings.md)
> 主密码只在 `sshwarden login` 中使用;不再有 `unlock --password`

兼容旧流程:`sshwarden ssh-config` 仍可打印建议 snippet,`sshwarden ssh-config --write` 仍可写入托管文件。
### ④ Key 与 Host 绑定(离线对象操作)

### 锁定 / 解锁
当 Bitwarden 里有多把 SSH Key 时,OpenSSH 可能会因为 `MaxAuthTries` 在尝试完所有 agent key 前断开连接。把 vault key 绑定到 SSH Host 可避免该问题(绑定会生成公开 `.pub` selector 文件与托管 SSH config):

```bash
sshwarden lock
sshwarden unlock --pin
sshwarden unlock --hello # Windows 当前可用
sshwarden unlock --password
sshwarden keys # 离线列出缓存的 key、绑定的 host、selector 与 ssh-config 状态
sshwarden keys bind github-key github.com # 用 key 名称或 vault item id 绑定 host
sshwarden keys unbind github-key github.com # 解绑单个 host
sshwarden keys unbind github-key --all # 解绑全部 host
sshwarden keys ui # 图形绑定管理器(需 agent 运行)
```

### 设置 PIN

```bash
sshwarden set-pin
```
也可以通过签名请求里的 **Bind & Approve...** 图形流程完成绑定。详见 [`docs/host-bindings.md`](docs/host-bindings.md)。

### 查看状态
### ⑤ 集成(本机设置)

```bash
sshwarden status
sshwarden env # 打印 shell 环境变量(sh/fish/powershell/cmd)
sshwarden ssh-config # 查看托管 snippet 路径与 Include 状态
sshwarden ssh-config show # 打印托管 snippet
sshwarden ssh-config write # 从本地缓存 + 绑定离线重写 snippet 并确保 Include 行
sshwarden ssh-config remove # 从 ~/.ssh/config 移除 Include 行
sshwarden startup enable # 登录时自启动(需先 `login` 建立 Remembered Device)
sshwarden startup disable # 取消自启动
sshwarden config # 显示配置文件路径
```

### Windows 自启动
### 典型首次流程

```bash
sshwarden daemon --install
sshwarden daemon --uninstall
sshwarden run --background # 启动后台 agent
sshwarden login # 登录 + 同步 + 按提示设置 PIN
sshwarden status # 确认状态
sshwarden startup enable # 可选:开机自启
```

Linux/macOS 自启动安装仍待实现。

## 与官方 Bitwarden Desktop 的关系

Bitwarden Desktop 已经提供 SSH Agent,但它依赖完整 Electron 桌面客户端。SSHWarden 的目标是提供一个更轻量的独立 agent。
Expand Down
48 changes: 37 additions & 11 deletions crates/sshwarden-agent/src/control.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
use serde::{Deserialize, Serialize};
use tracing::{error, info};

/// Returned by [`send_control_command`] when the control endpoint could not be
/// contacted at all — nothing is listening on the pipe/socket, so no daemon is
/// running. Kept distinct from post-connect failures (channel closed before the
/// reply arrived, malformed reply) so callers can tell "no daemon" apart from
/// "daemon present but the reply didn't make it back" — e.g. when the agent
/// tears the channel down during a clean `stop`.
#[derive(Debug)]
pub struct ControlUnreachable {
pub source: std::io::Error,
}

impl std::fmt::Display for ControlUnreachable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Failed to connect to SSHWarden daemon (is it running?): {}",
self.source
)
}
}

impl std::error::Error for ControlUnreachable {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ControlCommand {
pub cmd: String,
Expand Down Expand Up @@ -208,6 +235,9 @@ pub enum ControlAction {
pin: String,
},
Forget,
/// Cleanly shut down the daemon: cancel the main loop, stop the agent and
/// control server, and remove the PID file. Used by `stop` / `restart`.
Stop,
/// Open the host-binding management dialog. The daemon dispatches a
/// `UIRequest::BindHostsDialog` and responds once the dialog closes.
BindHostsDialog,
Expand Down Expand Up @@ -331,6 +361,7 @@ async fn dispatch_control_command(
"status-json" => ControlAction::Status { json: true },
"sync" => ControlAction::Sync,
"forget" => ControlAction::Forget,
"stop" => ControlAction::Stop,
"bind-hosts-dialog" => ControlAction::BindHostsDialog,
s if s.starts_with("unlock-pin:") => {
let pin = s.strip_prefix("unlock-pin:").unwrap_or("").to_string();
Expand Down Expand Up @@ -507,12 +538,9 @@ pub async fn send_control_command(cmd: &str) -> anyhow::Result<ControlResponse>
use tokio::net::windows::named_pipe::ClientOptions;

// Try to connect to the control pipe
let client = ClientOptions::new().open(CONTROL_PIPE_NAME).map_err(|e| {
anyhow::anyhow!(
"Failed to connect to SSHWarden daemon (is it running?): {}",
e
)
})?;
let client = ClientOptions::new()
.open(CONTROL_PIPE_NAME)
.map_err(|e| anyhow::Error::new(ControlUnreachable { source: e }))?;

let (reader, mut writer) = tokio::io::split(client);

Expand Down Expand Up @@ -543,11 +571,9 @@ pub async fn send_control_command(cmd: &str) -> anyhow::Result<ControlResponse>

let path = sshwarden_config::default_control_socket_path()?;
let stream = UnixStream::connect(&path).await.map_err(|e| {
anyhow::anyhow!(
"Failed to connect to SSHWarden daemon at {} (is it running?): {}",
path.display(),
e
)
anyhow::Error::new(ControlUnreachable {
source: std::io::Error::new(e.kind(), format!("{}: {e}", path.display())),
})
})?;

let (reader, mut writer) = tokio::io::split(stream);
Expand Down
13 changes: 6 additions & 7 deletions docs/host-bindings-followup.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Implementation progress:
## 1. System tray / menu-bar entry — deferred

**Why:** GUI users currently have to either (a) trigger the bind dialog
through a sign request, or (b) run `sshwarden bindings ui` from a terminal.
through a sign request, or (b) run `sshwarden keys ui` from a terminal.
Neither is great for users who never open a shell.

**Scope:**
Expand Down Expand Up @@ -106,15 +106,14 @@ line, portable mode interactions). Users need a single page they can read.
generated by sshwarden.
3. **Quickstart** —
```bash
sshwarden ssh-config install # one-time
sshwarden bindings add github-key github.com
ssh github.com # just works
sshwarden keys bind github-key github.com # also ensures the ~/.ssh/config Include
ssh github.com # just works
```
4. **GUI path** — first sign request → `Bind & Approve…` → done.
5. **File layout** — what gets written where (portable vs non-portable).
6. **Uninstalling** — `sshwarden ssh-config uninstall`, what's left behind.
6. **Uninstalling** — `sshwarden ssh-config remove`, what's left behind.
7. **Troubleshooting** —
- "Include line missing" → run `ssh-config install`
- "Include line missing" → run `ssh-config write`
- "Wrong key offered" → check `sshwarden ssh-config show`
- "Bind dialog shows no prefill" → cmdline read failed; explain when

Expand Down Expand Up @@ -151,7 +150,7 @@ These are not bugs per se — flag them during a real-world soak.
- **Snippet file path with spaces / non-ASCII:** fixed for both `Include` and
`IdentityFile`; status/uninstall also accept legacy unquoted Include lines.
- **Binding to `*` or other catch-all patterns:** warning added in CLI and GUI.
- **Concurrent edits:** two `sshwarden bindings add` invocations racing. The
- **Concurrent edits:** two `sshwarden keys bind` invocations racing. The
tmp-rename atomic save in `bindings.rs` prevents corruption but the second
writer's read can lose the first writer's add (read-modify-write race).
Add a file lock (`fs2::FileExt::try_lock_exclusive`) if this becomes a
Expand Down
22 changes: 10 additions & 12 deletions docs/host-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,21 @@ After logging in or syncing once so SSHWarden has a local key cache:

```bash
sshwarden login
sshwarden ssh-config install
sshwarden bindings add github-key github.com
sshwarden keys bind github-key github.com
ssh github.com
```

You can use either the key display name or the vault item id shown by `sshwarden keys` / `sshwarden bindings list`.
You can use either the key display name or the vault item id shown by `sshwarden keys`.

Useful commands:

```bash
sshwarden bindings list
sshwarden bindings add github-key github.com gitlab.com
sshwarden bindings remove github-key github.com
sshwarden bindings clear github-key
sshwarden ssh-config status
sshwarden keys # list keys + bindings + ssh-config status
sshwarden keys bind github-key github.com gitlab.com
sshwarden keys unbind github-key github.com
sshwarden keys unbind github-key --all
sshwarden ssh-config show
sshwarden ssh-config regenerate
sshwarden ssh-config write # regenerate snippet + ensure Include
```

## GUI path
Expand All @@ -80,7 +78,7 @@ In portable mode, SSHWarden's own config and selector files move to the portable
Remove the Include line:

```bash
sshwarden ssh-config uninstall
sshwarden ssh-config remove
```

This preserves `~/.ssh/sshwarden_config` and `bindings.json`. Delete them manually if you want a fully clean setup.
Expand All @@ -92,13 +90,13 @@ This preserves `~/.ssh/sshwarden_config` and `bindings.json`. Delete them manual
Run:

```bash
sshwarden ssh-config install
sshwarden ssh-config write
```

Then confirm with:

```bash
sshwarden ssh-config status
sshwarden ssh-config
```

### Wrong key offered
Expand Down
Loading
Loading