Skip to content

Commit 2bce66c

Browse files
committed
feat: Add HW key/OPK template support to web UI, auto-managed venv, README rewrite
- Web UI templates: modal-based creation with type selector (standard/sk/opk), interactive terminal for hardware key enrollment and opkssh login - Legacy device checkbox in new identity modal with security warning - Template list shows type/issuer metadata from .type/.issuer files - Backend websocket handler supports 'template' command type - Move ssh-ui Python app to lib/ssh-ui.py behind a bash wrapper that auto-creates a venv in lib/venv/ on first run - Move requirements-ui.txt to lib/ - install.sh copies lib/ assets (ssh-ui.py, requirements-ui.txt, ui/*) - README rewritten: anti-tracking focus, all commands documented, web UI section, architecture diagram, auto-venv notedEOF
1 parent 7ef5a80 commit 2bce66c

7 files changed

Lines changed: 1007 additions & 826 deletions

File tree

‎README.md‎

Lines changed: 125 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,165 @@
11
# ssh-unique-key
22

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.
44

5-
## Why this exists
5+
## The Problem
66

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:
88

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
1312

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
1524

1625
```bash
1726
./install.sh
1827
```
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
2040

2141
### Provision a new host
42+
2243
```bash
2344
ssh-new user@example.com
2445
```
25-
This scans the host, creates a unique identity, pushes the key, and logs you in.
2646

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+
2851
```bash
2952
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
3255
```
3356

3457
### Use OpenPubKey (opkssh)
58+
3559
```bash
3660
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
3962
ssh-new --template my-opk user@server.com
4063
```
4164

42-
## 🏗️ Architecture
65+
### Connect to legacy devices
4366

44-
The system uses a central storage in `~/.ssh/unique_keys`:
67+
```bash
68+
ssh-new --legacy user@old-cisco-router
69+
```
4570

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.
5672

57-
## 🔒 Security Considerations
73+
## Commands
5874

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:
6392

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+
```
6696

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.
7398

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:
78100

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.
82104

83-
## 🛠️ Commands
105+
The terminal integration handles interactive workflows (YubiKey touch prompts, OIDC browser login for opkssh) that would otherwise require the CLI.
84106

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.
94157

95-
## ⚙️ Configuration Structure
158+
### Hardware Tokens and OpenPubKey
96159

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.
98162

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
103164

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

Comments
 (0)