Showing 2 changed files with 335 additions and 58 deletions
+268 -0
KEYS_AND_ACCESS.md
@@ -0,0 +1,268 @@
1
+# SSH Keys and Access Guide
2
+
3
+Centralized reference for SSH key management and access paths.
4
+
5
+## Key Files Location
6
+
7
+All SSH keys are stored in `~/.ssh/keys/`:
8
+
9
+```
10
+~/.ssh/keys/
11
+  id_ed25519           → ~/.ssh/id_ed25519 (modern, preferred)
12
+  id_ed25519_2026-05
13
+  id_ed25519_2026-05.pub
14
+  id_rsa               (RSA legacy, 2048-bit)
15
+  id_rsa_old           (deprecated, from 2015)
16
+  id_rsa_old.pub
17
+  is-jumper_ed25519    (specific to is-jumper entry point)
18
+  is-jumper_ed25519.pub
19
+```
20
+
21
+## Key Usage Matrix
22
+
23
+| Key | File | Used By | Auth Method | Hosts |
24
+|-----|------|---------|-------------|-------|
25
+| **Modern ED25519** | `~/.ssh/id_ed25519` | most systems | pubkey | All modern hosts, J1, J2, local lab |
26
+| **is-jumper ED25519** | `~/.ssh/keys/is-jumper_ed25519` | is-jumper (entry point) | pubkey + IdentitiesOnly | Entry point `192.168.2.100` |
27
+| **Legacy RSA** | `~/.ssh/id_rsa` | legacy systems | pubkey | Old hosts still on RSA auth |
28
+| **Deprecated RSA** | `~/.ssh/keys/id_rsa_old` | final fallback | pubkey | Hosts not yet migrated (being phased out) |
29
+
30
+## Access Paths and Routing
31
+
32
+### 1. Local Lab Setup (192.168.2.0/24)
33
+
34
+**Direct access** — no jump needed:
35
+
36
+```
37
+is-jumper (192.168.2.100)
38
+├─ User: root
39
+├─ Port: 22
40
+├─ Key: ~/.ssh/keys/is-jumper_ed25519
41
+├─ IdentitiesOnly: yes
42
+└─ Routing: SSH_ROUTE=local
43
+
44
+Local dev machines (192.168.2.110-122)
45
+├─ User: bogdan
46
+├─ Port: 22
47
+└─ Key: ~/.ssh/id_ed25519
48
+```
49
+
50
+Access to is-jumper:
51
+
52
+```bash
53
+# Direct connection (SSH will use is-jumper_ed25519 via IdentitiesOnly)
54
+ssh is-jumper
55
+
56
+# Verify key in use:
57
+ssh -G is-jumper | grep identityfile
58
+```
59
+
60
+### 2. Production: Via is-jumper → J1/J2 → Final Hosts
61
+
62
+**Access chain for internal company network (10.253.51.0/24)**:
63
+
64
+```
65
+Your machine (local)
66
+  ↓ (ProxyJump is-jumper)
67
+is-jumper (192.168.2.100) [VPN client + key guardian]
68
+  ↓ (SSH forward)
69
+J1 (10.253.51.50:25904) or J2 (10.253.51.52:25904) [jump hosts]
70
+  ↓ (SSH forward)
71
+Final host (voip, porta, radius, etc.)
72
+```
73
+
74
+#### Step 1: is-jumper (entry point)
75
+
76
+- **User**: root
77
+- **Host**: 192.168.2.100
78
+- **Port**: 22
79
+- **Key**: `~/.ssh/keys/is-jumper_ed25519`
80
+- **IdentitiesOnly**: yes (forces use of specified key only)
81
+
82
+```bash
83
+ssh is-jumper
84
+```
85
+
86
+#### Step 2: J1/J2 (jump hosts)
87
+
88
+**Reached through is-jumper via ProxyJump**:
89
+
90
+- **User**: bogdan.timofte (jump host default)
91
+- **Hostname**: 10.253.51.50 (J1) or 10.253.51.52 (J2)
92
+- **Port**: 25904
93
+- **Key**: `~/.ssh/id_ed25519` (offered by SSH client)
94
+- **ProxyJump**: is-jumper (configured in ~/.ssh/config)
95
+
96
+```bash
97
+ssh j1        # Routes: local → is-jumper → J1
98
+ssh j2        # Routes: local → is-jumper → J2
99
+
100
+# Interactive jump host session:
101
+ssh -J is-jumper bogdan.timofte@10.253.51.50 -p 25904
102
+```
103
+
104
+**Verify J1 config**:
105
+
106
+```bash
107
+ssh -G j1 | grep -E '^(hostname|port|user|proxyjump|identityfile)'
108
+```
109
+
110
+#### Step 3: Final Hosts
111
+
112
+**From J1/J2 to actual hosts** (configured in J1's/J2's local SSH config):
113
+
114
+- **User**: varies by host group (bogdan, bogdan.timofte, root, etc.)
115
+- **Port**: 22 (standard) or 24 (jump hosts default) or custom
116
+- **Key**: `~/.ssh/id_ed25519`
117
+- **ProxyJump**: j1 or j2
118
+
119
+```bash
120
+# Example: PortaOne database server
121
+ssh porta-db        # → is-jumper → J1 → 193.16.148.11
122
+
123
+# Example: VoIP PBX
124
+ssh voip-prov       # → is-jumper → J1 → 10.253.51.139
125
+
126
+# Example: Radius database
127
+ssh falticeni.radius-db  # → is-jumper → J1 → falticeni.radius-db:24
128
+```
129
+
130
+### 3. Emergency Public Routes (j1.next-gen.ro / j2.next-gen.ro)
131
+
132
+If internal VPN is down, use public DNS names:
133
+
134
+```bash
135
+# Standard (internal VPN)
136
+ssh j1              # 10.253.51.50:25904
137
+
138
+# Emergency (public DNS)
139
+ssh j1.next-gen.ro  # j1.next-gen.ro:25904
140
+ssh j2.next-gen.ro  # j2.next-gen.ro:25904
141
+```
142
+
143
+Both routes go through is-jumper first (no direct connection).
144
+
145
+## Key Migration Status
146
+
147
+### Check which hosts have modern keys
148
+
149
+```bash
150
+# Test all local lab hosts
151
+for h in is-baobab is-ebony is-tapia is-jumper is-mazeri is-toltec is-andrafiabe is-anjohibe is-nasturel is-mat; do
152
+  timeout 2 ssh -o BatchMode=yes "$h" true 2>/dev/null && echo "$h: ✓" || echo "$h: ⚠"
153
+done
154
+```
155
+
156
+### Migrate a host to modern ED25519 key
157
+
158
+```bash
159
+# Automatic migration (uses legacy key to install modern key)
160
+tools/migrate-modern-key.sh is-baobab
161
+
162
+# Or migrate all
163
+tools/migrate-modern-key.sh
164
+```
165
+
166
+See [docs/KEY_MIGRATION.md](docs/KEY_MIGRATION.md) for manual procedures.
167
+
168
+## SSH Config Generation
169
+
170
+The `~/.ssh/config` is **auto-generated** from inventory — do not edit manually.
171
+
172
+```bash
173
+# Regenerate after inventory changes
174
+python3 tools/generate-configs.py
175
+
176
+# Deploy to ~/.ssh/config
177
+cp generated/client.conf ~/.ssh/config
178
+# or use the deploy script:
179
+tools/deploy-local.sh
180
+```
181
+
182
+**Config files generated**:
183
+
184
+| File | Target | Purpose |
185
+|------|--------|---------|
186
+| `generated/client.conf` | local client (~/.ssh/config) | local access config |
187
+| `generated/is-jumper.conf` | is-jumper (via deploy) | is-jumper alias config |
188
+| `generated/j1.conf` | J1 (server-side) | final host access on J1 |
189
+| `generated/j2.conf` | J2 (server-side) | final host access on J2 |
190
+
191
+## Troubleshooting
192
+
193
+### "Permission denied (publickey)"
194
+
195
+**Diagnosis**:
196
+
197
+```bash
198
+# Check which key is being offered
199
+ssh -vvv porta-db 2>&1 | grep -E "Offering|Authentications"
200
+
201
+# Check ProxyJump chain
202
+ssh -vvv j1 2>&1 | grep -E "ProxyJump|Connecting"
203
+```
204
+
205
+**Solutions**:
206
+
207
+1. **On is-jumper** — verify IdentitiesOnly and key:
208
+   ```bash
209
+   ssh -G is-jumper | grep identities
210
+   ```
211
+   Should show: `identitiesonly yes` and `identityfile ~/.ssh/keys/is-jumper_ed25519`
212
+
213
+2. **On J1/J2** — verify modern key is installed:
214
+   ```bash
215
+   ssh is-jumper "ssh-keygen -l -f /home/bogdan.timofte/.ssh/authorized_keys"
216
+   ```
217
+   Should include ED25519 fingerprints.
218
+
219
+3. **Regenerate and redeploy config**:
220
+   ```bash
221
+   python3 tools/generate-configs.py && cp generated/client.conf ~/.ssh/config
222
+   ```
223
+
224
+### "No route to host" (10.253.51.x)
225
+
226
+**Cause**: Trying to access internal IPs directly without is-jumper.
227
+
228
+**Solution**: Verify ProxyJump is configured:
229
+
230
+```bash
231
+ssh -G vo52 | grep proxyjump
232
+# Should output: proxyjump j1
233
+```
234
+
235
+### SSH hangs or timeouts
236
+
237
+**For jump hosts** (reduce to 5-10 seconds):
238
+
239
+```bash
240
+ssh -o ConnectTimeout=5 is-jumper
241
+ssh -o ConnectTimeout=5 j1
242
+```
243
+
244
+**Check network**:
245
+
246
+```bash
247
+# is-jumper reachable?
248
+ping 192.168.2.100
249
+
250
+# If behind firewall, may need port 22 open to is-jumper
251
+```
252
+
253
+## Maintenance Checklist
254
+
255
+- [ ] Keep both `id_ed25519` and `id_rsa_old` until all hosts migrated
256
+- [ ] After adding new hosts: run `tools/migrate-modern-key.sh <host>`
257
+- [ ] After inventory changes: regenerate with `python3 tools/generate-configs.py`
258
+- [ ] Periodically check: `ssh -G <alias>` to verify config without connecting
259
+- [ ] Never manually edit `~/.ssh/config` — it's auto-generated
260
+- [ ] Keep keys in `~/.ssh/keys/` with mode `600`
261
+
262
+## References
263
+
264
+- [Ed25519 vs RSA Security](https://ianix.com/pub/ed25519-deployment.html)
265
+- [OpenSSH Manual](https://man.openbsd.org/ssh)
266
+- [Project inventory structure](inventory/hosts.yaml)
267
+- [Key migration procedures](docs/KEY_MIGRATION.md)
268
+- [Architecture details](ARCHITECTURE.md) (legacy Romanian docs: `.doc/ssh-jump-architecture.md`)
+67 -58
README.md
@@ -1,34 +1,44 @@
1 1
 # SSH Infrastructure
2 2
 
3
-Source-controlled SSH routing and wrapper configuration for the Next-Gen jump
4
-infrastructure.
3
+Source-controlled SSH routing and configuration for multi-user jump infrastructure
4
+(`is-jumper` gateway → J1/J2 company network → final hosts).
5 5
 
6
-## Runtime vs project
6
+**⚠️ New to this project?** Start with [KEYS_AND_ACCESS.md](KEYS_AND_ACCESS.md) — it
7
+explains which SSH key to use where, and how the access chain works.
7 8
 
8
-- Project source: `~/Documents/Workspaces/Bogdan/ssh-infrastructure`
9
-- OpenSSH runtime: `~/.ssh`
9
+## Quick Start
10 10
 
11
-Keep secrets and machine-local state out of this repository:
11
+```bash
12
+# 1. Access the local jump gateway
13
+ssh is-jumper
14
+
15
+# 2. From there, reach company network via J1 or J2
16
+ssh j1              # or: ssh j2
17
+
18
+# 3. From J1, reach final hosts
19
+ssh porta-db        # automatic routing: local → is-jumper → J1 → porta-db
20
+```
21
+
22
+The `~/.ssh/config` is auto-generated from `inventory/hosts.yaml` — edit the
23
+inventory, not the config file.
24
+
25
+## File Structure
12 26
 
13
-- private keys: `~/.ssh/keys/`
14
-- `authorized_keys`
15
-- `known_hosts`
16
-- socket or agent state
27
+- **Project source**: `~/Documents/Workspaces/Bogdan/ssh-infrastructure`
28
+- **OpenSSH runtime**: `~/.ssh` (do not commit)
17 29
 
18
-The runtime `~/.ssh/config` is a generated single-file OpenSSH config. The
19
-project is the place to edit wrappers, documentation, inventory, and generator
20
-code.
30
+Keep secrets and machine-local state out of version control:
21 31
 
22
-Deploy the local runtime copy with:
32
+- private keys: `~/.ssh/keys/` (not in git)
33
+- `authorized_keys`, `known_hosts`, socket state (not in git)
34
+
35
+Deploy the local runtime with:
23 36
 
24 37
 ```bash
38
+# Regenerate ~/.ssh/config from inventory + install wrappers
25 39
 tools/deploy-local.sh
26 40
 ```
27 41
 
28
-The deploy script generates `generated/client.conf`, installs it as
29
-`~/.ssh/config`, and installs the wrapper commands into `~/.local/bin`.
30
-It does not touch keys, `authorized_keys`, or `known_hosts`.
31
-
32 42
 ## Version control
33 43
 
34 44
 This directory is the git repository for source files only. Generated configs,
@@ -44,17 +54,19 @@ git commit
44 54
 
45 55
 ## SSH Key Management
46 56
 
47
-Modern key migration from legacy infrastructure:
57
+Which key goes where? See [KEYS_AND_ACCESS.md](KEYS_AND_ACCESS.md) for the full matrix.
58
+
59
+Migrate hosts from legacy RSA to modern ED25519:
48 60
 
49 61
 ```bash
50
-# Migrate all legacy hosts to modern key (ed25519)
62
+# Migrate all legacy hosts
51 63
 tools/migrate-modern-key.sh
52 64
 
53
-# Migrate specific host
65
+# Migrate a specific host
54 66
 tools/migrate-modern-key.sh is-baobab
55 67
 ```
56 68
 
57
-See [docs/KEY_MIGRATION.md](docs/KEY_MIGRATION.md) for detailed procedures.
69
+Details: [docs/KEY_MIGRATION.md](docs/KEY_MIGRATION.md)
58 70
 
59 71
 ## Current client layout
60 72
 
@@ -82,49 +94,46 @@ tools/generate-configs.py
82 94
 The `generated/*.conf` files are deploy artifacts. They are ignored by git and
83 95
 can be recreated at any time with `tools/deploy-local.sh`.
84 96
 
85
-## Sync from upstream
97
+## Inventory and Config Generation
86 98
 
87
-Pull the upstream inventory, apply this machine's local `is-jumper` key override,
88
-regenerate, and deploy if the inventory changed:
99
+The single source of truth:
89 100
 
90
-```bash
91
-tools/sync-hosts-from-upstream.sh
101
+```text
102
+inventory/hosts.yaml         ← edit here to add/modify hosts
103
+  ↓ (python3 tools/generate-configs.py)
104
+generated/client.conf        ← deployed to ~/.ssh/config
105
+generated/is-jumper.conf     ← deployed to is-jumper
106
+generated/j1.conf            ← deployed to J1
107
+generated/j2.conf            ← deployed to J2
92 108
 ```
93 109
 
94
-Defaults:
110
+### User and Port Defaults
95 111
 
96
-```text
97
-UPSTREAM_SSH_TARGET=nextgen@192.168.2.103
98
-UPSTREAM_HOSTS_PATH=/home/nextgen/projects/ssh-infrastructure/inventory/hosts.yaml
99
-LOCAL_IS_JUMPER_IDENTITY_FILE=~/.ssh/keys/is-jumper_ed25519
100
-DEPLOY_AFTER_SYNC=1
101
-```
112
+| Context | User | Port | Notes |
113
+| --- | --- | --- | --- |
114
+| Jump hosts (J1, J2) | `bogdan.timofte` | `24` (standard) or `25904` (VPN) | override in inventory |
115
+| Final hosts | `bogdan` | `22` | most systems; dotted usernames cause issues |
116
+| Interactive auth (Cisco, OLTs) | varies | `22` | marked with `auth: password_interactive` |
102 117
 
103
-Useful overrides:
118
+### Deployment
104 119
 
105 120
 ```bash
106
-UPSTREAM_HOSTS_FILE=/tmp/hosts.yaml tools/sync-hosts-from-upstream.sh
107
-DEPLOY_AFTER_SYNC=0 tools/sync-hosts-from-upstream.sh
108
-FORCE_DEPLOY=1 tools/sync-hosts-from-upstream.sh
121
+# Option 1: Full deploy (recommended)
122
+tools/deploy-local.sh
123
+
124
+# Option 2: Manual steps
125
+python3 tools/generate-configs.py           # Regenerate configs
126
+cp generated/client.conf ~/.ssh/config      # Install client config
109 127
 ```
110 128
 
111
-Known defaults captured there:
112
-
113
-- jump hosts default to user `bogdan.timofte`;
114
-- jump hosts default to port `24`, with explicit overrides such as `25904`;
115
-- final hosts default to user `bogdan` because most distributions do not like
116
-  dotted local usernames;
117
-- when imported configs disagree between `bogdan` and `root`, `bogdan` wins;
118
-- final hosts default to port `22`, with explicit overrides where needed.
119
-- Cisco/TACACS router entries use `auth: password_interactive`; generated SSH
120
-  config marks them so the wrapper does not force `BatchMode=yes`.
121
-- Imported IFS devices use the canonical hostname already known on the jump
122
-  servers as client-side `HostName`, while IPs remain available as aliases for
123
-  autocomplete.
124
-- J1/J2 configs inherit the company-managed global compatibility block and
125
-  selected `Match Host` user/port defaults from the jump servers instead of
126
-  duplicating them in generated output.
127
-- J1/J2 configs only emit host entries when there is an effective override to
128
-  keep on the jump side; hosts that only use inherited defaults are omitted.
129
-- J1/J2 generated configs are stripped down to functional SSH stanzas only,
130
-  without generated comments or group annotations.
129
+### Sync from Upstream
130
+
131
+Pull latest `hosts.yaml` from nextgen and redeploy:
132
+
133
+```bash
134
+tools/sync-hosts-from-upstream.sh
135
+
136
+# Customize:
137
+DEPLOY_AFTER_SYNC=0 tools/sync-hosts-from-upstream.sh  # generate only
138
+UPSTREAM_SSH_TARGET=user@host tools/sync-hosts-from-upstream.sh  # custom source
139
+```