|
1 | 1 | # ssh-unique-key |
2 | 2 |
|
3 | | -A production-ready toolkit to manage SSH identities based on **cryptographic host identity** rather than just hostnames. |
| 3 | +Automatically manages unique SSH keypairs per host so you can't be tracked by your public key. |
4 | 4 |
|
5 | | -## Why this exists |
| 5 | +## The Problem |
6 | 6 |
|
7 | | -Traditional SSH key management relies on `~/.ssh/known_hosts` and assumes that if a hostname (`db-prod`) points to a new IP, it's the same server. In cloud environments, `db-prod` might be rebuilt daily with a new identity. |
| 7 | +Most people use a single SSH keypair across all their servers. That public key becomes a global identifier — it sits in `authorized_keys` on every host you access. Anyone who compromises one server (or has legitimate access to it) can take your public key and: |
8 | 8 |
|
9 | | -**ssh-unique-key** changes the paradigm: |
10 | | -1. **One Key Per Host:** Every host gets its own unique key pair (or a specific template key). |
11 | | -2. **Identity First:** Hosts are recognized by their public key. If you move a server to a new domain name, this tool knows it's the *same* server and uses the correct credentials. |
12 | | -3. **Scanning Mitigation:** Since your public key is unique to that specific host, attackers cannot scan the internet to find other servers you have access to. |
| 9 | +- Search other compromised hosts' `authorized_keys` for the same key |
| 10 | +- Scan the internet (Shodan, Censys) for SSH servers that accept it |
| 11 | +- Correlate your access across unrelated systems |
13 | 12 |
|
14 | | -## 🚀 Quick Start |
| 13 | +Your SSH public key is effectively a tracking token. |
| 14 | + |
| 15 | +## How ssh-unique-key Solves This |
| 16 | + |
| 17 | +- **One key per host.** Every server gets its own unique keypair, generated automatically. Your public key on Server A exists nowhere else — it can't be used to find Server B. |
| 18 | +- **Zero friction.** `ssh-new user@host` handles everything: scanning, key generation, deployment, and config. You keep using `ssh` normally. |
| 19 | +- **Host identity by key.** As a side benefit, hosts are identified by their cryptographic public key rather than hostname. Rename a server or change its IP — the tool tracks it correctly. |
| 20 | +- **Template support.** Use standard keys, hardware tokens (FIDO2/YubiKey), or OpenPubKey (opkssh with OIDC providers) across hosts. |
| 21 | +- **Web UI.** Browser-based management with an integrated terminal for interactive operations (hardware key enrollment, opkssh login flows). |
| 22 | + |
| 23 | +## Installation |
15 | 24 |
|
16 | 25 | ```bash |
17 | 26 | ./install.sh |
18 | 27 | ``` |
19 | | -Ensure `~/bin` is in your `$PATH`. |
| 28 | + |
| 29 | +This installs commands to `~/bin` (ensure it's in your `$PATH`) and stores key data in `~/.ssh/unique_keys`. |
| 30 | + |
| 31 | +Options: |
| 32 | + |
| 33 | +- `./install.sh --install-xterm` — include xterm.js assets for the web UI terminal |
| 34 | +- `./install.sh --skip-xterm` — skip xterm.js assets |
| 35 | +- `./install.sh uninstall` — remove all symlinks and installed files |
| 36 | + |
| 37 | +The web UI requires Python 3. Dependencies are installed automatically in a virtual environment on first run of `ssh-ui`. |
| 38 | + |
| 39 | +## Quick Start |
20 | 40 |
|
21 | 41 | ### Provision a new host |
| 42 | + |
22 | 43 | ```bash |
23 | 44 | ssh-new user@example.com |
24 | 45 | ``` |
25 | | -This scans the host, creates a unique identity, pushes the key, and logs you in. |
26 | 46 |
|
27 | | -### Use a Template (e.g., for hardware keys) |
| 47 | +Scans the host, creates a unique identity, generates a keypair, pushes the public key, and logs you in. |
| 48 | + |
| 49 | +### Use a hardware key (YubiKey) |
| 50 | + |
28 | 51 | ```bash |
29 | 52 | ssh-template create work-yubikey |
30 | | -ssh-template generate-sk work-yubikey # Generates hardware-backed key |
31 | | -ssh-new --template work-yubikey --comment "Touch YubiKey" user@server.com |
| 53 | +ssh-template generate-sk work-yubikey |
| 54 | +ssh-new --template work-yubikey user@server.com |
32 | 55 | ``` |
33 | 56 |
|
34 | 57 | ### Use OpenPubKey (opkssh) |
| 58 | + |
35 | 59 | ```bash |
36 | 60 | ssh-template create my-opk |
37 | | -# Follow prompts to login (supports 'google', 'gitlab', 'microsoft' shortcuts) and import |
38 | | -ssh-template generate-opk my-opk |
| 61 | +ssh-template generate-opk my-opk # Supports 'google', 'gitlab', 'microsoft' shortcuts |
39 | 62 | ssh-new --template my-opk user@server.com |
40 | 63 | ``` |
41 | 64 |
|
42 | | -## 🏗️ Architecture |
| 65 | +### Connect to legacy devices |
43 | 66 |
|
44 | | -The system uses a central storage in `~/.ssh/unique_keys`: |
| 67 | +```bash |
| 68 | +ssh-new --legacy user@old-cisco-router |
| 69 | +``` |
45 | 70 |
|
46 | | -1. **`host-uuid/` (Source of Truth):** |
47 | | - * Named by the SHA256 hash of the host's public key. |
48 | | - * Contains `config` (host options), `known_host_keys`, and user keys. |
49 | | -2. **`by-key/` (Secure Lookup):** |
50 | | - * Symlinks based on the base64 host key (e.g., `%K`). |
51 | | - * Used by SSH clients that support the `%K` token (OpenSSH). |
52 | | - * **Trusts the lookup:** Disables `StrictHostKeyChecking` because reaching this path proves the host identity is verified. |
53 | | -3. **`by-host/` (Fallback Lookup):** |
54 | | - * Symlinks based on hostname (`%h`). |
55 | | - * Used for new hosts (before key is known) or on systems lacking `%K` support (stock macOS). |
| 71 | +Enables deprecated algorithms (DSA, RSA-SHA1, CBC ciphers) only for that specific host, keeping your global SSH config secure. |
56 | 72 |
|
57 | | -## 🔒 Security Considerations |
| 73 | +## Commands |
58 | 74 |
|
59 | | -### 1. The Trust Model |
60 | | -This tool separates trust into two layers: |
61 | | -* **Cryptographic Path Trust (`by-key`):** When SSH resolves a config via `%K`, we **disable** `StrictHostKeyChecking`. Why? Because the directory path *is* the check. We only create that symlink if the host's key hash matches our stored `host-uuid`. This eliminates annoying "Host key changed" errors for valid renames while maintaining cryptographic integrity. |
62 | | -* **Standard Trust (`by-host`):** When falling back to hostname lookup, standard `known_hosts` verification is active. |
| 75 | +| Command | Description | |
| 76 | +| :--- | :--- | |
| 77 | +| `ssh-new` | Scan host, create identity, generate keys, deploy, and connect. | |
| 78 | +| `ssh-del` | Remove keys for a user. Cleans up the host identity if no users remain. | |
| 79 | +| `ssh-conf` | Edit host-specific SSH options (Port, Forwarding, etc.). | |
| 80 | +| `ssh-template` | Manage key templates (standard, hardware, or OpenPubKey). | |
| 81 | +| `ssh-rotate` | Rotate host keys when a server's host key changes. | |
| 82 | +| `ssh-user-rotate` | Rotate a user's keypair for a specific host. | |
| 83 | +| `ssh-template-rotate` | Rotate keys within a template (ed25519, ecdsa, or rsa). | |
| 84 | +| `ssh-backup` | Create an encrypted archive of the key store. | |
| 85 | +| `ssh-restore` | Restore keys from a backup archive. | |
| 86 | +| `ssh-history` | View the operations log. | |
| 87 | +| `ssh-ui` | Launch the web-based management interface. | |
| 88 | + |
| 89 | +## Web UI |
| 90 | + |
| 91 | +Start the web UI: |
63 | 92 |
|
64 | | -### 2. MITM Protection |
65 | | -`ssh-new` permanently stores the server's full public key set. Every time you run `ssh-new` (e.g., to add a user or update an alias), it re-scans and `diff`s the keys. If they have changed, it aborts with a **SECURITY WARNING**. |
| 93 | +```bash |
| 94 | +ssh-ui |
| 95 | +``` |
66 | 96 |
|
67 | | -### 3. Blast Radius Reduction |
68 | | -By default, `ssh-new` generates a unique keypair for every single host. |
69 | | -* **Scenario:** You access 100 servers. |
70 | | -* **Compromise:** Server A is hacked and your private key *for that server* is stolen. |
71 | | -* **Impact:** The attacker gains access to... Server A. |
72 | | -* **Scanning:** The attacker cannot take that public key and scan the internet (Shodan/Censys) to find your other 99 servers, because that key exists nowhere else. |
| 97 | +Starts a local web server on a random port (localhost only). A URL with a one-time authentication token is printed on startup. The token is exchanged for an HTTP-only cookie on first visit, then stripped from the URL. |
73 | 98 |
|
74 | | -### 4. Legacy Systems |
75 | | -Some older devices (Cisco routers, old Linux) only support deprecated algorithms (DSA, RSA-SHA1, CBC ciphers). |
76 | | -* **Risk:** Enabling these globally weakens your security posture. |
77 | | -* **Solution:** Use `ssh-new --legacy user@old-host`. This injects the insecure algorithms **only** into that specific host's config file (`host-uuid/<hash>/config`), leaving your default SSH configuration secure. |
| 99 | +Features: |
78 | 100 |
|
79 | | -### 5. Hardware Tokens (YubiKey) & OpenPubKey |
80 | | -* `ssh-template generate-sk` creates FIDO2/U2F backed keys (`ed25519-sk`). |
81 | | -* `ssh-template generate-opk` integrates with OpenPubKey (OIDC). `ssh-new` will automatically validate your certificate and trigger `opkssh login` (optionally with a specific Issuer) if your session has expired. The script verifies that the credential file was actually updated before connecting. |
| 101 | +- **Dashboard** — overview of all host identities, their users, and keys. Connect, rotate, or delete directly from the browser. |
| 102 | +- **Templates** — create and manage key templates. Hardware key enrollment and opkssh login run in an embedded terminal (xterm.js over websockets). |
| 103 | +- **History** — searchable log of all operations. |
82 | 104 |
|
83 | | -## 🛠️ Commands |
| 105 | +The terminal integration handles interactive workflows (YubiKey touch prompts, OIDC browser login for opkssh) that would otherwise require the CLI. |
84 | 106 |
|
85 | | -| Command | Description | |
86 | | -| :--- | :--- | |
87 | | -| `ssh-new` | Main tool. Scans host, creates keys/config, links everything. | |
88 | | -| `ssh-del` | Safely removes keys for a user. Cleans up host if empty. | |
89 | | -| `ssh-template` | Manage templates (standard, hardware, or OPK). | |
90 | | -| `ssh-conf` | Edit host-specific options (Port, Forwarding). | |
91 | | -| `ssh-history` | View operations log. | |
92 | | -| `ssh-backup` | Backup key store to archive. | |
93 | | -| `ssh-restore` | Restore keys from archive. | |
| 107 | +## Architecture |
| 108 | + |
| 109 | +All data lives under `~/.ssh/unique_keys`: |
| 110 | + |
| 111 | +```text |
| 112 | +~/.ssh/unique_keys/ |
| 113 | + host-uuid/<sha256-hash>/ # One directory per host, named by host key hash |
| 114 | + config # Host-specific SSH config |
| 115 | + known_host_keys # Stored host public keys |
| 116 | + <user>/ # Per-user keypairs |
| 117 | + by-key/<base64-key> -> # Symlinks by host public key (for %K token) |
| 118 | + by-host/<hostname> -> # Symlinks by hostname (fallback) |
| 119 | + templates/<name>/ # Key templates |
| 120 | + config-top.d/ # User config overrides (loaded first) |
| 121 | + config-bottom.d/ # Global defaults (loaded last) |
| 122 | + history.log # Operations log |
| 123 | +``` |
| 124 | + |
| 125 | +Your `~/.ssh/config` gets these includes: |
| 126 | + |
| 127 | +```text |
| 128 | +Include config-top.d/* # Your overrides |
| 129 | +Include by-key/%K # Cryptographic lookup (preferred) |
| 130 | +Include by-host/%h # Hostname fallback |
| 131 | +Include config-bottom.d/* # Global defaults |
| 132 | +``` |
| 133 | + |
| 134 | +### Lookup Paths |
| 135 | + |
| 136 | +**`by-key/%K` (primary):** When SSH connects and knows the host's public key, it resolves config via the base64-encoded key. This path is the identity proof itself — if the symlink exists, the host is verified. `StrictHostKeyChecking` is disabled on this path because the directory structure *is* the check. |
| 137 | + |
| 138 | +**`by-host/%h` (fallback):** For first connections (before the key is known) or on systems without `%K` support (stock macOS SSH). Standard `known_hosts` verification applies. |
| 139 | + |
| 140 | +## Security Model |
| 141 | + |
| 142 | +### Anti-Tracking |
| 143 | + |
| 144 | +The core goal. Each host gets a unique keypair, so your public key on one server can never be correlated with your access to any other server. There is no shared identifier across hosts. |
| 145 | + |
| 146 | +### Blast Radius Reduction |
| 147 | + |
| 148 | +A compromised key on Server A gives the attacker access to Server A only. The key exists nowhere else — no other server's `authorized_keys` contains it, and no internet scan (Shodan, Censys) can find your other servers using it. |
| 149 | + |
| 150 | +### MITM Protection |
| 151 | + |
| 152 | +`ssh-new` stores the server's full public key set on first contact. On subsequent runs (adding users, updating aliases), it re-scans and diffs the keys. If they've changed, it aborts with a security warning. |
| 153 | + |
| 154 | +### Legacy Device Isolation |
| 155 | + |
| 156 | +The `--legacy` flag injects deprecated algorithms only into the specific host's config file, not your global SSH configuration. Your security posture for modern hosts is unaffected. |
94 | 157 |
|
95 | | -## ⚙️ Configuration Structure |
| 158 | +### Hardware Tokens and OpenPubKey |
96 | 159 |
|
97 | | -Your `~/.ssh/config` will look like this: |
| 160 | +- `generate-sk` creates FIDO2/U2F-backed keys (`ed25519-sk`) requiring physical touch. |
| 161 | +- `generate-opk` integrates with OpenPubKey via opkssh. `ssh-new` validates certificates and triggers `opkssh login` (with optional issuer) when sessions expire, verifying the credential was updated before connecting. |
98 | 162 |
|
99 | | -1. `Include config-top.d/*` (User overrides) |
100 | | -2. `Include by-key/%K` (Secure path) |
101 | | -3. `Include by-host/%h` (Fallback path) |
102 | | -4. `Include config-bottom.d/*` (Global defaults) |
| 163 | +## License |
103 | 164 |
|
104 | | -This ensures that specific secure lookups take precedence, but you can still apply global settings. |
| 165 | +See [LICENSE](LICENSE) for details. |
0 commit comments