|
1 | | -# fssh |
| 1 | +# fssh - Secure and Simple SSH Key Management |
2 | 2 |
|
3 | 3 |  |
4 | 4 |
|
5 | | -A solution for macOS that eliminates the need to manually unlock private keys with passphrases every time you use public key authentication, and helps you quickly view and connect to SSH hosts configured in `~/.ssh/config` without having to open the config file each time. |
| 5 | +## What is fssh? |
6 | 6 |
|
7 | | -When SSH connects to a remote host, it prompts for fingerprint authentication, which decrypts and imports the private key upon successful verification. Additionally, fssh provides an interactive shell where you can quickly view and connect to hosts defined in `~/.ssh/config`. |
| 7 | +fssh is a macOS-only SSH key management tool that solves two common pain points: |
8 | 8 |
|
9 | | -## Use Cases |
10 | | -- Plaintext SSH keys stored locally on disk pose security risks |
11 | | -- Encrypted keys require entering passphrases every time you connect, which is inconvenient |
12 | | -- When multiple hosts are configured in `~/.ssh/config`, their aliases are easy to forget over time, requiring you to check the config file before each connection |
| 9 | +1. **Entering private key passphrase every SSH login** → fssh lets you unlock with Touch ID or One-Time Password (OTP) |
| 10 | +2. **Forgetting server aliases in `~/.ssh/config`** → fssh provides an interactive shell with Tab completion for quick connections |
13 | 11 |
|
14 | | -## Solutions |
15 | | -- Automatically prompts for Touch ID fingerprint verification when using SSH commands to connect to hosts on macOS, then uses the master key to decrypt SSH private keys for authentication |
16 | | -- Compatible with OpenSSH's ssh-agent |
17 | | -- Running `fssh` directly launches an interactive shell where you can use commands like `list` and `connect` to view host information from `~/.ssh/config`. In this shell, you can enter a host's ID, host name, or IP directly to connect via SSH |
| 12 | +## How It Works |
| 13 | + |
| 14 | +``` |
| 15 | +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
| 16 | +│ SSH Client │ ──▶ │ fssh Agent │ ──▶ │Remote Server│ |
| 17 | +└─────────────┘ └─────────────┘ └─────────────┘ |
| 18 | + │ |
| 19 | + Touch ID or OTP |
| 20 | + unlocks encrypted |
| 21 | + private keys |
| 22 | +``` |
| 23 | + |
| 24 | +Your SSH private keys are stored encrypted. They can only be decrypted after Touch ID fingerprint or OTP verification. |
18 | 25 |
|
19 | 26 | ## Screenshots |
20 | | -SSH connection with fingerprint unlock: |
21 | 27 |
|
22 | | - |
| 28 | +**Touch ID prompt during SSH login:** |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +**Interactive shell for viewing and connecting to servers:** |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Quick Start |
| 39 | + |
| 40 | +### Step 1: Install |
| 41 | + |
| 42 | +```bash |
| 43 | +# After downloading the source code, build it |
| 44 | +go build ./cmd/fssh |
| 45 | + |
| 46 | +# Install to system directory (requires admin privileges) |
| 47 | +sudo cp fssh /usr/local/bin/ |
| 48 | +``` |
| 49 | + |
| 50 | +### Step 2: Initialize |
| 51 | + |
| 52 | +Choose an authentication mode based on your device: |
| 53 | + |
| 54 | +**If your Mac has Touch ID (MacBook Pro/Air 2016+, iMac with Magic Keyboard, etc.):** |
| 55 | + |
| 56 | +```bash |
| 57 | +fssh init --mode touchid |
| 58 | +``` |
| 59 | + |
| 60 | +**If your Mac doesn't have Touch ID (Mac Mini, older Macs, VMs, etc.):** |
| 61 | + |
| 62 | +```bash |
| 63 | +fssh init --mode otp |
| 64 | +``` |
| 65 | + |
| 66 | +During OTP mode initialization: |
| 67 | +1. You'll set a password (at least 12 characters) |
| 68 | +2. A TOTP secret will be displayed - add it to an authenticator app (e.g., Google Authenticator, Authy) |
| 69 | +3. 10 recovery codes will be shown - **save them securely** |
| 70 | + |
| 71 | +### Step 3: Import SSH Private Key |
23 | 72 |
|
24 | | -Viewing hosts from `~/.ssh/config` in the interactive shell: |
| 73 | +```bash |
| 74 | +# Import your SSH private key (you'll be prompted for passphrase if the key has one) |
| 75 | +fssh import --alias mykey --file ~/.ssh/id_rsa --ask-passphrase |
| 76 | +``` |
| 77 | + |
| 78 | +Parameters: |
| 79 | +- `--alias`: A name for this key for easy reference |
| 80 | +- `--file`: Path to the private key file |
| 81 | +- `--ask-passphrase`: Add this if the private key is passphrase-protected |
25 | 82 |
|
26 | | - |
| 83 | +### Step 4: Start the Agent |
27 | 84 |
|
28 | | -Connecting to a host from the interactive shell: |
| 85 | +```bash |
| 86 | +fssh agent |
| 87 | +``` |
29 | 88 |
|
30 | | - |
| 89 | +Once started, the Agent runs in the background, listening on `~/.fssh/agent.sock`. |
31 | 90 |
|
32 | | -## Features |
33 | | -- Touch ID/Local authentication to read master key (stored in Keychain) |
34 | | -- AES-256-GCM + HKDF encryption for private keys (independent `salt`/`nonce` per file) |
35 | | -- Private key import/export (PKCS#8 PEM backup), listing and status checking |
36 | | -- ssh-agent: |
37 | | - - Supports fingerprint verification per signature, or configure TTL for repeat access within a short time window |
38 | | - - Compatible with RSA-SHA2 (rsa-sha2-256/512) |
39 | | -- Interactive Shell: parses `~/.ssh/config` hosts, Tab completion, default connection behavior |
40 | | -- Configuration generator: automatically generates local `~/.ssh/config` entries with `IdentityAgent` |
| 91 | +### Step 5: Configure SSH to Use fssh Agent |
41 | 92 |
|
42 | | -## Installation & Configuration |
43 | | -1. Build: `go build ./cmd/fssh`; install to `/usr/local/bin/fssh` |
44 | | -2. Run `fssh init` to initialize the master key |
45 | | -3. Run `fssh import -alias <string> -file <path/to/private> --ask-passphrase` to import private keys |
46 | | -4. Start the SSH authentication agent: `fssh agent --unlock-ttl-seconds 600` |
47 | | -5. Modify the `~/.ssh/config` file so all SSH connections go through the fssh agent (you can also use `export SSH_AUTH_SOCK=~/.fssh/agent.sock` to set the variable for specific use cases) |
| 93 | +Edit `~/.ssh/config` and add at the **very beginning**: |
48 | 94 |
|
49 | | -Configure the OpenSSH agent by adding the following at the beginning of `~/.ssh/config`: |
50 | 95 | ``` |
51 | | -host * |
52 | | - ServerAliveInterval 30 |
53 | | - AddKeysToAgent yes |
54 | | - ControlPersist 60 |
55 | | - ControlMaster auto |
56 | | - IdentityAgent ~/.fssh/agent.sock |
| 96 | +Host * |
| 97 | + IdentityAgent ~/.fssh/agent.sock |
| 98 | +``` |
| 99 | + |
| 100 | +This routes all SSH connections through fssh Agent. |
| 101 | + |
| 102 | +### Step 6: Start Using |
| 103 | + |
| 104 | +```bash |
| 105 | +# Use SSH normally - Touch ID or OTP prompt will appear automatically |
| 106 | +ssh user@yourserver.com |
57 | 107 | ``` |
58 | 108 |
|
59 | | -6. If needed, create and modify the configuration file: `~/.fssh/config.json`, example: |
| 109 | +--- |
| 110 | + |
| 111 | +## Auto-Start on Login |
| 112 | + |
| 113 | +Tired of manually starting the Agent after each reboot? Set up auto-start: |
| 114 | + |
| 115 | +```bash |
| 116 | +# Copy the launch configuration file |
| 117 | +cp contrib/com.fssh.agent.plist ~/Library/LaunchAgents/ |
| 118 | + |
| 119 | +# Load the service |
| 120 | +launchctl load -w ~/Library/LaunchAgents/com.fssh.agent.plist |
60 | 121 | ``` |
| 122 | + |
| 123 | +**Check service status:** |
| 124 | + |
| 125 | +```bash |
| 126 | +launchctl list | grep fssh |
| 127 | +``` |
| 128 | + |
| 129 | +Normal output looks like: `- 0 com.fssh.agent` (0 means running) |
| 130 | + |
| 131 | +**To restart the service:** |
| 132 | + |
| 133 | +```bash |
| 134 | +launchctl kickstart -k gui/$(id -u)/com.fssh.agent |
| 135 | +``` |
| 136 | + |
| 137 | +**To stop the service:** |
| 138 | + |
| 139 | +```bash |
| 140 | +launchctl unload ~/Library/LaunchAgents/com.fssh.agent.plist |
| 141 | +``` |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## Interactive Shell |
| 146 | + |
| 147 | +Run `fssh` or `fssh shell` to enter interactive mode: |
| 148 | + |
| 149 | +```bash |
| 150 | +$ fssh |
| 151 | +fssh> list # List all hosts from ~/.ssh/config |
| 152 | +fssh> search prod # Search for hosts containing "prod" |
| 153 | +fssh> connect myserver # Connect to myserver |
| 154 | +fssh> myserver # Or just type the hostname to connect |
| 155 | +fssh> exit # Exit the shell |
| 156 | +``` |
| 157 | + |
| 158 | +**Tab completion** is supported - type partial hostname and press Tab to autocomplete. |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Command Reference |
| 163 | + |
| 164 | +| Command | Description | |
| 165 | +|---------|-------------| |
| 166 | +| `fssh init --mode touchid` | Initialize (Touch ID mode) | |
| 167 | +| `fssh init --mode otp` | Initialize (OTP mode) | |
| 168 | +| `fssh import --alias name --file path --ask-passphrase` | Import a private key | |
| 169 | +| `fssh list` | List imported keys | |
| 170 | +| `fssh export --alias name --out path` | Export a key (backup) | |
| 171 | +| `fssh remove --alias name` | Remove a key | |
| 172 | +| `fssh agent` | Start the Agent | |
| 173 | +| `fssh status` | Check status | |
| 174 | +| `fssh shell` | Enter interactive shell | |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## Configuration |
| 179 | + |
| 180 | +Configuration file location: `~/.fssh/config.json` |
| 181 | + |
| 182 | +```json |
61 | 183 | { |
62 | | - "socket":"~/.fssh/agent.sock", |
63 | | - "require_touch_id_per_sign":true, |
64 | | - "unlock_ttl_seconds":600, |
65 | | - "log_level":"info", |
66 | | - "log_format":"plain" |
| 184 | + "socket": "~/.fssh/agent.sock", |
| 185 | + "require_touch_id_per_sign": true, |
| 186 | + "unlock_ttl_seconds": 600, |
| 187 | + "log_level": "info", |
| 188 | + "log_format": "plain" |
67 | 189 | } |
68 | 190 | ``` |
69 | 191 |
|
70 | | -- socket: SSH agent socket location |
71 | | -- require_touch_id_per_sign: Whether to require Touch ID verification on every SSH signature |
72 | | - - true: Security mode enabled, requires Touch ID on each signature (or after TTL expires) |
73 | | - - false: Convenience mode, decrypts all keys once during startup and keeps them in memory |
74 | | -- unlock_ttl_seconds: Cache time window after Touch ID unlock |
75 | | -- log_level: Controls log output level |
76 | | - - debug: Shows all logs (including cache hit information) |
77 | | - - info: Shows general information (default) |
78 | | - - warn: Shows warnings and errors only |
79 | | - - error: Shows only errors |
80 | | -- log_format: Controls log output format |
81 | | - - plain: Human-readable plain format |
82 | | - - json: Structured JSON format |
83 | | - |
84 | | -## Auto-start on Login |
85 | | -- Start agent: `fssh agent --unlock-ttl-seconds 600` |
86 | | -- Auto-start: Copy `contrib/com.fssh.agent.plist` to `~/Library/LaunchAgents/` and run `launchctl load -w`; after modifying configuration, use `launchctl kickstart -k gui/$(id -u)/com.fssh.agent` to reload |
| 192 | +**Configuration options:** |
87 | 193 |
|
88 | | -## Interactive Shell |
89 | | -- Launch: `fssh` or `fssh shell` |
90 | | -- Commands: |
91 | | - - `list` displays `id\thost(ip)` |
92 | | - - `search <term>` filters by id/host/ip |
93 | | - - `connect <id|host|ip>` initiates connection; non-command input defaults to connection |
94 | | - - Tab completion covers commands and id/host/ip |
| 194 | +| Option | Description | Default | |
| 195 | +|--------|-------------|---------| |
| 196 | +| `socket` | Agent socket path | `~/.fssh/agent.sock` | |
| 197 | +| `require_touch_id_per_sign` | Require verification for each SSH signature (secure mode) | `true` | |
| 198 | +| `unlock_ttl_seconds` | Cache duration after verification (seconds) - no re-verification needed within this period | `600` (10 min) | |
| 199 | +| `log_level` | Log level: `debug`/`info`/`warn`/`error` | `info` | |
| 200 | +| `log_format` | Log format: `plain` (readable) / `json` (structured) | `plain` | |
| 201 | + |
| 202 | +**Secure Mode vs Convenience Mode:** |
| 203 | + |
| 204 | +- `require_touch_id_per_sign: true` (Secure): Verification required for each SSH connection (or within TTL cache period) |
| 205 | +- `require_touch_id_per_sign: false` (Convenience): All keys decrypted once at startup, no further verification needed |
| 206 | + |
| 207 | +--- |
95 | 208 |
|
96 | 209 | ## Troubleshooting |
97 | | -- `"incorrect signature type / no mutual signature supported"` |
98 | | - - Confirm agent is running and set `SSH_AUTH_SOCK=~/.fssh/agent.sock` |
99 | | - - Local entries should include `IdentityAgent ~/.fssh/agent.sock` |
100 | | - - Server accepts RSA-SHA2 (use `sshd-align` or manually edit) |
101 | | -- Input invisible after connection: use `ssh -tt` and suspend line editing during remote session |
102 | | -- Logging: configure `log_out/log_err`, `log_level`, `log_format`; restart agent after changes |
| 210 | + |
| 211 | +### 1. Error "incorrect signature type" or "no mutual signature supported" |
| 212 | + |
| 213 | +**Cause**: SSH client isn't using fssh Agent, or server doesn't support RSA-SHA2. |
| 214 | + |
| 215 | +**Solution**: |
| 216 | +1. Verify Agent is running: `launchctl list | grep fssh` |
| 217 | +2. Verify `~/.ssh/config` has `IdentityAgent ~/.fssh/agent.sock` |
| 218 | +3. Or set environment variable: `export SSH_AUTH_SOCK=~/.fssh/agent.sock` |
| 219 | + |
| 220 | +### 2. No input display (cursor doesn't move) |
| 221 | + |
| 222 | +**Cause**: Terminal control issue. |
| 223 | + |
| 224 | +**Solution**: Use `ssh -tt` to force TTY allocation: |
| 225 | + |
| 226 | +```bash |
| 227 | +ssh -tt user@server |
| 228 | +``` |
| 229 | + |
| 230 | +### 3. launchctl load error "Load failed: 5: Input/output error" |
| 231 | + |
| 232 | +**Cause**: Service already loaded, or initialization incomplete. |
| 233 | + |
| 234 | +**Solution**: |
| 235 | + |
| 236 | +```bash |
| 237 | +# Unload first |
| 238 | +launchctl unload ~/Library/LaunchAgents/com.fssh.agent.plist |
| 239 | + |
| 240 | +# Ensure initialization is complete |
| 241 | +fssh init --mode touchid # or otp |
| 242 | + |
| 243 | +# Reload |
| 244 | +launchctl load -w ~/Library/LaunchAgents/com.fssh.agent.plist |
| 245 | +``` |
| 246 | + |
| 247 | +### 4. OTP verification code always wrong |
| 248 | + |
| 249 | +**Cause**: Phone time out of sync, or TOTP was added incorrectly. |
| 250 | + |
| 251 | +**Solution**: |
| 252 | +1. Ensure phone time is correct (enable automatic time setting) |
| 253 | +2. Delete the old entry in authenticator app and re-add |
| 254 | +3. If recovery is impossible, use recovery codes |
| 255 | + |
| 256 | +### 5. Forgot OTP password? |
| 257 | + |
| 258 | +If you saved recovery codes, use them to reset. Without recovery codes, you must reinitialize (losing imported keys): |
| 259 | + |
| 260 | +```bash |
| 261 | +# Warning: This deletes all imported keys! |
| 262 | +rm -rf ~/.fssh |
| 263 | +fssh init --mode otp |
| 264 | +# Then re-import your keys |
| 265 | +``` |
| 266 | + |
| 267 | +--- |
103 | 268 |
|
104 | 269 | ## Security Notes |
105 | | -- Security mode: per-signature unlock (or TTL cache) avoids long-term decrypted private keys in memory |
106 | | -- Convenience mode: decrypts and keeps in memory on startup; only use this when prompted too frequently |
107 | | -- Avoid plaintext leakage: don't store plaintext keys/passwords in repositories or logs |
| 270 | + |
| 271 | +1. **Encrypted key storage**: Imported private keys are encrypted with AES-256-GCM - even if your computer is stolen, keys can't be decrypted without Touch ID/OTP |
| 272 | +2. **Enable FileVault**: macOS full-disk encryption provides additional protection |
| 273 | +3. **Protect recovery codes**: OTP recovery codes are like master keys - store them securely |
| 274 | +4. **Regular backups**: Use `fssh export` to backup important keys |
| 275 | + |
| 276 | +--- |
| 277 | + |
| 278 | +## Technical Details |
| 279 | + |
| 280 | +- **Encryption**: AES-256-GCM + HKDF (independent salt/nonce per key file) |
| 281 | +- **Key derivation**: PBKDF2 (100,000 iterations) |
| 282 | +- **TOTP standard**: RFC 6238 |
| 283 | +- **Compatibility**: Fully compatible with OpenSSH ssh-agent protocol |
| 284 | + |
| 285 | +--- |
108 | 286 |
|
109 | 287 | ## Credits |
110 | | -- This project is assisted by TRAE AI software |
| 288 | + |
| 289 | +This project was developed with assistance from TRAE AI software. |
0 commit comments