Showing 4 changed files with 595 additions and 750 deletions
+0 -222
.doc/ssh-jump-architecture.md
@@ -1,222 +0,0 @@
1
-# Arhitectura SSH Curenta
2
-
3
-Acest document descrie configuratia activa din `/home/nextgen/.ssh`. Sursa versionata a proiectului este in `/home/nextgen/projects/ssh-infrastructure`; `/home/nextgen/.ssh` ramane runtime OpenSSH si nu contine repo-ul git. Ultima actualizare: 2026-05-15. Jurnalul istoric `ssh-jump-attempts.md` este obsolete si poate fi consultat doar din git.
4
-
5
-## Structura Locala
6
-
7
-```text
8
-/home/nextgen/.ssh/
9
-  config                        <- single-file generat din inventory/hosts.yaml
10
-  known_hosts
11
-  known_hosts.old
12
-  authorized_keys
13
-  keys/
14
-    id_ed25519
15
-    id_ed25519.pub
16
-    elastix-root.pub
17
-  .doc/
18
-    ssh-jump-architecture.md
19
-
20
-~/.local/bin/
21
-  ssh
22
-  scp
23
-  sftp
24
-```
25
-
26
-Reguli:
27
-
28
-- Proiectul versionat sta in `~/projects/ssh-infrastructure`; `~/.ssh` ramane runtime OpenSSH.
29
-- Cheile de identitate stau in `~/.ssh/keys`.
30
-- Hosturile SSH sunt definite in `inventory/hosts.yaml`, apoi generate in `generated/*.conf`.
31
-- `~/.ssh/config` este instalat din `generated/client.conf` prin `tools/deploy-local.sh`; nu se editeaza manual.
32
-- Scripturile sursa stau versionate in `scripts/` in repo.
33
-- `~/.local/bin/` contine copiile executabile instalate de `tools/deploy-local.sh`; nu se editeaza manual.
34
-- `authorized_keys`, `known_hosts` si `config` raman in radacina `.ssh`, pentru compatibilitate cu OpenSSH.
35
-- `j1-relay.sh`, `ssh-proxy.sh`, `ensure-ssh-agent-bridge.sh` si `ensure-ssh-jump.sh` au fost eliminate; `ssh-wrapper.sh` ruleaza clientul SSH activ pe `is-jumper`, unde se afla cheia fizica.
36
-
37
-## Wrappers locale
38
-
39
-Scripturile sursa sunt in `scripts/` in repo; `~/.local/bin/` contine copiile executabile instalate local.
40
-
41
-| Comanda | Script | Mecanism |
42
-| --- | --- | --- |
43
-| `ssh` | `ssh-wrapper.sh` | parseaza args, rezolva hostul via `ssh -G`, construieste sesiunea nested |
44
-| `scp` | `scp-wrapper.sh` | apeleaza `/usr/bin/scp -S ~/.local/bin/ssh` — rutarea e delegata wrapper-ului ssh |
45
-| `sftp` | `sftp-wrapper.sh` | apeleaza `/usr/bin/sftp -S ~/.local/bin/ssh` — idem |
46
-
47
-- Compatibilitatea SSH pentru hop-ul final este responsabilitatea serverelor `J1`/`J2`; wrapperele locale nu reproduc algoritmi, KEX, cipher-uri sau `OPENSSL_CONF`.
48
-- Daca PATH-ul include `~/.local/bin` inaintea `/usr/bin`, comenzile `ssh`/`scp`/`sftp` folosesc automat aceste wrappers.
49
-
50
-## Arhitectura rețelei
51
-
52
-```
53
-192.168.2.0/24  — rețea locală de birou
54
-  is-toltec  (workstation-ul local)
55
-  is-jumper  192.168.2.100  (gardian cheie + client VPN)
56
-
57
-10.253.51.0/24  — rețea internă companie (acces via VPN de pe is-jumper)
58
-  J1  10.253.51.50:25904
59
-  J2  10.253.51.52:25904
60
-  hosturi finale (voip, porta, radius etc.)
61
-```
62
-
63
-**is-jumper** (192.168.2.100) este pe rețeaua locală de birou — accesibil direct din is-toltec, **fără VPN**. Este un **client** VPN (nu server): prin el se obține acces la `10.253.51.0/24`. Este **gardianul cheii fizice** (card RSA 4096) deoarece deservește mai mulți utilizatori din `192.168.2.0/24`.
64
-
65
-**Cheia fizică** (cardul) este montată exclusiv pe is-jumper și expusă prin GPG agent la `/run/user/0/gnupg/S.gpg-agent.ssh`. Wrapper-ul nu mai forwardează acest socket pe mașina locală; în schimb intră pe `is-jumper`, setează `SSH_AUTH_SOCK` la socketul local de acolo și rulează de pe `is-jumper` clientul SSH către J1/J2/j1/j2.
66
-
67
-## Lanțul de acces
68
-
69
-Calea standard (VPN activ, J1):
70
-
71
-```text
72
-local
73
-  -> is-jumper (192.168.2.100, executor SSH + cheie fizica)
74
-  -> J1 (10.253.51.50:25904)
75
-  -> host final
76
-```
77
-
78
-Calea publică (urgențe, fără rută VPN, j1/j2):
79
-
80
-```text
81
-local
82
-  -> is-jumper (192.168.2.100, executor SSH + cheie fizica)
83
-  -> j1.next-gen.ro:25904
84
-  -> host final
85
-```
86
-
87
-Sesiuni interactive pe J1/J2:
88
-
89
-```text
90
-local -> is-jumper -> J1
91
-local -> is-jumper -> J2
92
-```
93
-
94
-Nu mai exista bridge local de agent (`/tmp/is-jumper-agent.sock`). Cheia fizica ramane folosita local pe `is-jumper`.
95
-
96
-Jumps disponibile via flaguri wrapper:
97
-
98
-| Flag | Jump | Rută | Când |
99
-| --- | --- | --- | --- |
100
-| implicit / `-J1` | J1 (10.253.51.50) | client SSH rulat pe is-jumper | standard |
101
-| `-J2` | J2 (10.253.51.52) | client SSH rulat pe is-jumper | failover VPN |
102
-| `-j1` | j1.next-gen.ro | client SSH rulat pe is-jumper | urgențe |
103
-| `-j2` | j2.next-gen.ro | client SSH rulat pe is-jumper | urgențe |
104
-
105
-## Scripturi
106
-
107
-### ssh-wrapper.sh
108
-
109
-Parseaza argumentele `ssh`, rezolva hostul via `ssh -G`, construieste sesiunea nested activa pe `is-jumper`:
110
-
111
-```
112
-exec /usr/bin/ssh is-jumper "SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh ssh -A <jump_host> '<final_ssh_cmd>'"
113
-```
114
-
115
-Bypass automat (pass-through direct) pentru:
116
-- `is-jumper` / `192.168.2.100`
117
-- Flaguri de interogare: `-G`, `-Q`, `-V`, `--help`
118
-
119
-Pentru J1/J2/j1/j2, wrapper-ul conecteaza intai local la `is-jumper`, apoi ruleaza acolo `ssh -A` cu `SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh`. Pentru hosturile finale, comanda de pe jump ruleaza inca un `ssh` catre hostul final. Pentru `scp`/`sftp`, wrapper-ul pastreaza forma de subsystem `-s ... sftp`.
120
-
121
-### scp-wrapper.sh / sftp-wrapper.sh
122
-
123
-Wrappers subtiri care injecteaza `-S ~/.local/bin/ssh` la apelul sistemului:
124
-
125
-```bash
126
-/usr/bin/scp -S ~/.local/bin/ssh "$@"
127
-/usr/bin/sftp -S ~/.local/bin/ssh "$@"
128
-```
129
-
130
-Rutarea este delegata integral catre `ssh-wrapper.sh`; `scp`/`sftp` raman transparente pentru utilizator.
131
-
132
-Nu mai exista scripturi active bazate pe Python, ProxyJump sau port-forwarding externe; vechiul helper Python a fost eliminat dupa ce a fost semnalat de Sentinel.
133
-
134
-## Config SSH
135
-
136
-`~/.ssh/config` este un single-file generat din `inventory/hosts.yaml`:
137
-
138
-```sshconfig
139
-# Generated by tools/generate-configs.py.
140
-# Do not edit this file directly; edit inventory/hosts.yaml.
141
-```
142
-
143
-Generatorul produce:
144
-
145
-| Fisier | Destinatie | Rol |
146
-| --- | --- | --- |
147
-| `generated/client.conf` | client local | config runtime pentru `~/.ssh/config` |
148
-| `generated/is-jumper.conf` | is-jumper | config pentru jump aliases |
149
-| `generated/j1.conf` | J1 | hosturi finale |
150
-| `generated/j2.conf` | J2 | hosturi finale |
151
-
152
-Reguli de configurare:
153
-
154
-- hosturile finale definesc doar `HostName`, `User` si `Port`;
155
-- pe client, hosturile importate din IFS folosesc drept `HostName` numele
156
-  canonic deja cunoscut pe jump, iar IP-ul ramane alias pentru autocomplete;
157
-- `is-jumper` defineste si cheia locala de acces (`IdentityFile`, `IdentitiesOnly`);
158
-- wrapperul construieste ruta nested prin `is-jumper -> J1` implicit;
159
-- optiunile comune per-grup (ConnectTimeout etc.) stau in `inventory/hosts.yaml`;
160
-- `generated/j1.conf` si `generated/j2.conf` mostenesc blocul global company-managed
161
-  si omit `User`/`Port` cand toate aliasurile unui host sunt deja acoperite de
162
-  regulile `Match Host` de pe jump;
163
-- `generated/j1.conf` si `generated/j2.conf` nu mai includ hosturile care folosesc
164
-  doar defaulturi mostenite; raman doar override-urile efective si exceptiile
165
-  per-pattern care trebuie pastrate pe jump;
166
-- `generated/j1.conf` si `generated/j2.conf` sunt generate fara comentarii,
167
-  doar cu stanza-urile SSH necesare;
168
-- hosturile Radius folosesc acelasi flux nested prin J1 ca restul hosturilor finale.
169
-
170
-Aliasuri principale:
171
-
172
-| Alias | Rol | Rutare |
173
-| --- | --- | --- |
174
-| `is-jumper` | VPN gateway + gardian cheie fizica | direct (IP 192.168.2.100) |
175
-| `J1`, `J2` | jump hosts, login interactiv | wrapper prin is-jumper |
176
-| `voip-prov`, `voip-prov-root` | host VoIP provider | wrapper nested prin J1 |
177
-| `porta-sip`, `porta-web`, `porta-db`, `porta-configurator` | PortaOne MR30 | wrapper nested prin J1 |
178
-| `elastix` | host vechi | wrapper nested prin J1 |
179
-| `*.radius-db`, `*.radius-pppoe` | baze Radius regionale | wrapper nested prin J1 |
180
-
181
-`J1` si `J2` nu mai folosesc `ProxyJump`, `IdentityAgent` sau `RemoteCommand` in configul local; wrapper-ul orchestreaza explicit conexiunea prin `is-jumper`.
182
-
183
-## PortaOne
184
-
185
-Aliasuri PortaOne:
186
-
187
-- `porta-sip`, `p12-sip`, `p12`, `p12.voip.ro` -> `193.16.148.4`
188
-- `porta-web`, `porta-api`, `porta-slave`, `porta7`, `telefonie.next-gen.ro` -> `193.16.148.7`
189
-- `porta-db`, `porta-master`, `porta1` -> `193.16.148.11`
190
-- `porta-configurator`, `porta-config` -> `193.16.148.13`
191
-
192
-Aceste hosturi folosesc ruta nested din wrapper; eventualele optiuni de compatibilitate ale hop-ului final sunt asigurate pe `J1`/`J2`.
193
-
194
-## Verificari
195
-
196
-Verificare configuratie fara conectare:
197
-
198
-```bash
199
-ssh -G is-jumper | grep -E '^(hostname|user|identityfile|identitiesonly)'
200
-ssh -G porta-db | grep -E '^(hostname|user|port)'
201
-ssh -G falticeni.radius-db | grep -E '^(hostname|user|port|connecttimeout)'
202
-```
203
-
204
-Verificare read-only cu conectare:
205
-
206
-```bash
207
-ssh porta-db hostname
208
-ssh porta-sip 'service porta-sip status'
209
-ssh voip-prov hostname
210
-scp porta-db:/etc/hosts /tmp/test-scp
211
-sftp porta-db <<< 'ls /'
212
-```
213
-
214
-## Mentenanta
215
-
216
-- Cand adaugi o cheie noua, pune-o in `~/.ssh/keys` si seteaza permisiuni `600` pentru cheia privata.
217
-- Cand adaugi un host nou: adauga-l in `inventory/hosts.yaml`, ruleaza generatorul si apoi `tools/deploy-local.sh`.
218
-- Cand adaugi un domeniu nou: adauga un grup nou in `inventory/hosts.yaml`.
219
-- Cand adaugi un wrapper nou: adauga scriptul in `scripts/` in repo si lasa `tools/deploy-local.sh` sa il instaleze in `~/.local/bin/`.
220
-- Nu readuce `ProxyJump`, `IdentityAgent /tmp/is-jumper-agent.sock`, helperul Python sau port-forwarding per-host.
221
-- Nu activa `ForwardAgent yes` pe hosturile finale fara un motiv explicit.
222
-- Dupa modificari la wrappers sau config, verifica: `ssh -G <alias>` si cel putin un alias read-only (`ssh porta-db hostname`).
+0 -327
KEYS_AND_ACCESS.md
@@ -1,327 +0,0 @@
1
-# SSH Keys and Access Guide
2
-
3
-Centralized reference for SSH key management and access paths.
4
-
5
-## Key Storage
6
-
7
-### Local Keys (on your machine)
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
-### Physical Hardware Key (on is-jumper)
22
-
23
-**⚠️ IMPORTANT**: The primary authentication for the **company network (J1/J2)** is a **physical hardware security key (card/smartcard)** mounted **only on is-jumper** (192.168.2.100).
24
-
25
-- **Location**: is-jumper (192.168.2.100) only
26
-- **Type**: Hardware security module / smartcard (RSA 4096-bit)
27
-- **Exposed via**: GPG agent at `/run/user/0/gnupg/S.gpg-agent.ssh` (on is-jumper)
28
-- **Access method**: SSH agent forwarding through is-jumper
29
-- **Users served**: Multiple users from the 192.168.2.0/24 network
30
-
31
-**You cannot access J1/J2 directly from your local machine.** The connection must go through is-jumper, which holds the only valid physical key for the company network.
32
-
33
-## Key Usage Matrix
34
-
35
-| Key | Location | Used By | Purpose | Auth Method |
36
-| --- | --- | --- | --- | --- |
37
-| **Physical card (RSA 4096)** | Hardware on is-jumper only | J1, J2, company network | Primary auth for nextgen network | Hardware smartcard via GPG agent |
38
-| **is-jumper ED25519** | `~/.ssh/keys/is-jumper_ed25519` | Your machine → is-jumper | Access the entry point | pubkey + IdentitiesOnly |
39
-| **Modern ED25519** | `~/.ssh/id_ed25519` | Local lab, final hosts | Secondary/fallback auth | pubkey |
40
-| **Legacy RSA** | `~/.ssh/id_rsa` | Legacy systems | Transitional | pubkey |
41
-| **Deprecated RSA** | `~/.ssh/keys/id_rsa_old` | Very old hosts | Final fallback | pubkey |
42
-
43
-**Key insight**: You authenticate TO is-jumper with your local ED25519 key, but once on is-jumper, the system uses the **physical hardware key** to authenticate to the company network (J1/J2).
44
-
45
-## Access Paths and Routing
46
-
47
-### 1. Local Lab Setup (192.168.2.0/24)
48
-
49
-**Direct access** — no jump needed:
50
-
51
-```
52
-is-jumper (192.168.2.100)
53
-├─ User: root
54
-├─ Port: 22
55
-├─ Key: ~/.ssh/keys/is-jumper_ed25519
56
-├─ IdentitiesOnly: yes
57
-└─ Routing: SSH_ROUTE=local
58
-
59
-Local dev machines (192.168.2.110-122)
60
-├─ User: bogdan
61
-├─ Port: 22
62
-└─ Key: ~/.ssh/id_ed25519
63
-```
64
-
65
-Access to is-jumper:
66
-
67
-```bash
68
-# Direct connection (SSH will use is-jumper_ed25519 via IdentitiesOnly)
69
-ssh is-jumper
70
-
71
-# Verify key in use:
72
-ssh -G is-jumper | grep identityfile
73
-```
74
-
75
-### 2. Production: Via is-jumper → J1/J2 → Final Hosts
76
-
77
-**Access chain for internal company network (10.253.51.0/24)**:
78
-
79
-```
80
-Your machine (local)
81
-  ↓ (ED25519 pubkey)
82
-is-jumper (192.168.2.100) [VPN client + physical card key guardian]
83
-  ↓ (SSH agent forwarding + physical card RSA 4096)
84
-J1 (10.253.51.50:25904) or J2 (10.253.51.52:25904) [jump hosts]
85
-  ↓ (SSH forward)
86
-Final host (voip, porta, radius, etc.)
87
-```
88
-
89
-**Critical**: You cannot connect to J1/J2 directly from your local machine because the physical hardware key is **only on is-jumper**. All company network access must go through is-jumper first.
90
-
91
-#### Step 1: is-jumper (entry point)
92
-
93
-Authenticate to is-jumper with your local ED25519 key:
94
-
95
-- **User**: root
96
-- **Host**: 192.168.2.100
97
-- **Port**: 22
98
-- **Key**: `~/.ssh/keys/is-jumper_ed25519`
99
-- **IdentitiesOnly**: yes (forces use of specified key only)
100
-
101
-```bash
102
-ssh is-jumper
103
-```
104
-
105
-Once logged in, is-jumper has access to the physical card key via GPG agent at `/run/user/0/gnupg/S.gpg-agent.ssh`.
106
-
107
-#### Step 2: J1/J2 (jump hosts) — via is-jumper's physical key
108
-
109
-**SSH agent forwarding** connects your SSH client to is-jumper's physical card:
110
-
111
-- **Routing**: ProxyJump is-jumper
112
-- **Authentication on J1/J2**: Physical card (RSA 4096) via `SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh`
113
-- **User on J1/J2**: `bogdan.timofte` (company network default)
114
-- **Port**: 25904 (VPN jump port)
115
-
116
-```bash
117
-ssh j1        # Routes: local → is-jumper → J1 (using is-jumper's physical key)
118
-ssh j2        # Routes: local → is-jumper → J2 (using is-jumper's physical key)
119
-```
120
-
121
-**The SSH config handles this automatically** — you don't need to manually set up agent forwarding. The wrapper or ProxyJump chain uses is-jumper's physical key to authenticate.
122
-
123
-**Verify J1 config**:
124
-
125
-```bash
126
-ssh -G j1 | grep -E '^(hostname|port|user|proxyjump)'
127
-```
128
-
129
-#### Step 3: Final Hosts
130
-
131
-**From J1/J2 to actual hosts** (configured in J1's/J2's local SSH config):
132
-
133
-- **User**: varies by host group (bogdan, bogdan.timofte, root, etc.)
134
-- **Port**: 22 (standard) or 24 (jump hosts default) or custom
135
-- **Key**: `~/.ssh/id_ed25519`
136
-- **ProxyJump**: j1 or j2
137
-
138
-```bash
139
-# Example: PortaOne database server
140
-ssh porta-db        # → is-jumper → J1 → 193.16.148.11
141
-
142
-# Example: VoIP PBX
143
-ssh voip-prov       # → is-jumper → J1 → 10.253.51.139
144
-
145
-# Example: Radius database
146
-ssh falticeni.radius-db  # → is-jumper → J1 → falticeni.radius-db:24
147
-```
148
-
149
-### 3. Emergency Public Routes (j1.next-gen.ro / j2.next-gen.ro)
150
-
151
-If internal VPN is down, use public DNS names:
152
-
153
-```bash
154
-# Standard (internal VPN)
155
-ssh j1              # 10.253.51.50:25904
156
-
157
-# Emergency (public DNS)
158
-ssh j1.next-gen.ro  # j1.next-gen.ro:25904
159
-ssh j2.next-gen.ro  # j2.next-gen.ro:25904
160
-```
161
-
162
-Both routes go through is-jumper first (no direct connection).
163
-
164
-## Key Migration Status
165
-
166
-### Check which hosts have modern keys
167
-
168
-```bash
169
-# Test all local lab hosts
170
-for h in is-baobab is-ebony is-tapia is-jumper is-mazeri is-toltec is-andrafiabe is-anjohibe is-nasturel is-mat; do
171
-  timeout 2 ssh -o BatchMode=yes "$h" true 2>/dev/null && echo "$h: ✓" || echo "$h: ⚠"
172
-done
173
-```
174
-
175
-### Migrate a host to modern ED25519 key
176
-
177
-```bash
178
-# Automatic migration (uses legacy key to install modern key)
179
-tools/migrate-modern-key.sh is-baobab
180
-
181
-# Or migrate all
182
-tools/migrate-modern-key.sh
183
-```
184
-
185
-See [docs/KEY_MIGRATION.md](docs/KEY_MIGRATION.md) for manual procedures.
186
-
187
-## SSH Config Generation
188
-
189
-The `~/.ssh/config` is **auto-generated** from inventory — do not edit manually.
190
-
191
-```bash
192
-# Regenerate after inventory changes
193
-python3 tools/generate-configs.py
194
-
195
-# Deploy to ~/.ssh/config
196
-cp generated/client.conf ~/.ssh/config
197
-# or use the deploy script:
198
-tools/deploy-local.sh
199
-```
200
-
201
-**Config files generated**:
202
-
203
-| File | Target | Purpose |
204
-|------|--------|---------|
205
-| `generated/client.conf` | local client (~/.ssh/config) | local access config |
206
-| `generated/is-jumper.conf` | is-jumper (via deploy) | is-jumper alias config |
207
-| `generated/j1.conf` | J1 (server-side) | final host access on J1 |
208
-| `generated/j2.conf` | J2 (server-side) | final host access on J2 |
209
-
210
-## Troubleshooting
211
-
212
-### "Permission denied (publickey)" on J1/J2
213
-
214
-**⚠️ First check**: Do you have **access to is-jumper**?
215
-
216
-```bash
217
-ssh is-jumper "echo access ok"
218
-```
219
-
220
-If this fails, the issue is your local ED25519 key or is-jumper authentication. Fix that first.
221
-
222
-**If is-jumper works but J1 fails**:
223
-
224
-The issue is likely the **physical card key is not properly exposed** or **SSH agent forwarding isn't working**.
225
-
226
-**Diagnosis**:
227
-
228
-```bash
229
-# 1. Check if you can reach is-jumper
230
-ssh is-jumper
231
-
232
-# 2. From is-jumper, verify the physical card is available
233
-ssh is-jumper "ls -la /run/user/0/gnupg/S.gpg-agent.ssh"
234
-# Should exist and be readable
235
-
236
-# 3. Check J1 connectivity with verbose output
237
-ssh -vvv j1 2>&1 | grep -E "ProxyJump|Offering|Authentications|Trying"
238
-```
239
-
240
-**Solutions**:
241
-
242
-1. **is-jumper's SSH agent not running**:
243
-   ```bash
244
-   ssh is-jumper "ps aux | grep gpg-agent"
245
-   # If not running, restart it or contact the system administrator
246
-   ```
247
-
248
-2. **SSH config not set up for agent forwarding**:
249
-   - Verify ProxyJump is configured: `ssh -G j1 | grep proxyjump`
250
-   - Verify `ForwardAgent yes` is set (check generated config)
251
-
252
-3. **Physical card key not accessible**:
253
-   - This is a system issue on is-jumper — contact the key administrator
254
-   - The card may need to be re-mounted or re-authenticated
255
-
256
-4. **SSH on your machine not supporting agent forwarding**:
257
-   ```bash
258
-   # Ensure SSH_AUTH_SOCK is set when you SSH to is-jumper
259
-   ssh -A is-jumper
260
-   ```
261
-
262
-### "Permission denied (publickey)" on is-jumper
263
-
264
-**The issue**: Your local ED25519 key isn't authorized on is-jumper.
265
-
266
-**Verify your key is present**:
267
-
268
-```bash
269
-ls -la ~/.ssh/keys/is-jumper_ed25519
270
-cat ~/.ssh/keys/is-jumper_ed25519.pub
271
-```
272
-
273
-**Check that is-jumper has your key**:
274
-
275
-```bash
276
-ssh is-jumper "cat ~/.ssh/authorized_keys | grep -i ed25519"
277
-```
278
-
279
-If your key isn't there, it needs to be added by the is-jumper administrator.
280
-
281
-### "No route to host" (10.253.51.x)
282
-
283
-**Cause**: Trying to reach J1/J2 directly without going through is-jumper.
284
-
285
-**Check ProxyJump**:
286
-
287
-```bash
288
-ssh -G j1 | grep proxyjump
289
-# Must show: proxyjump is-jumper
290
-```
291
-
292
-If not, regenerate config: `python3 tools/generate-configs.py && cp generated/client.conf ~/.ssh/config`
293
-
294
-### SSH hangs or timeouts
295
-
296
-**For jump hosts** (reduce to 5-10 seconds):
297
-
298
-```bash
299
-ssh -o ConnectTimeout=5 is-jumper
300
-ssh -o ConnectTimeout=5 j1
301
-```
302
-
303
-**Check network**:
304
-
305
-```bash
306
-# is-jumper reachable?
307
-ping 192.168.2.100
308
-
309
-# If behind firewall, may need port 22 open to is-jumper
310
-```
311
-
312
-## Maintenance Checklist
313
-
314
-- [ ] Keep both `id_ed25519` and `id_rsa_old` until all hosts migrated
315
-- [ ] After adding new hosts: run `tools/migrate-modern-key.sh <host>`
316
-- [ ] After inventory changes: regenerate with `python3 tools/generate-configs.py`
317
-- [ ] Periodically check: `ssh -G <alias>` to verify config without connecting
318
-- [ ] Never manually edit `~/.ssh/config` — it's auto-generated
319
-- [ ] Keep keys in `~/.ssh/keys/` with mode `600`
320
-
321
-## References
322
-
323
-- [Ed25519 vs RSA Security](https://ianix.com/pub/ed25519-deployment.html)
324
-- [OpenSSH Manual](https://man.openbsd.org/ssh)
325
-- [Project inventory structure](inventory/hosts.yaml)
326
-- [Key migration procedures](docs/KEY_MIGRATION.md)
327
-- [Architecture details](ARCHITECTURE.md) (legacy Romanian docs: `.doc/ssh-jump-architecture.md`)
+595 -75
README.md
@@ -1,139 +1,659 @@
1
-# SSH Infrastructure
1
+# SSH Infrastructure - Single Source of Truth
2 2
 
3
-Source-controlled SSH routing and configuration for multi-user jump infrastructure
4
-(`is-jumper` gateway → J1/J2 company network → final hosts).
3
+Last updated: 2026-05-21
5 4
 
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.
5
+This is the only project documentation file. Keep architecture, key handling,
6
+sync/deploy steps, troubleshooting, and maintenance notes here. Do not add
7
+separate Markdown documents for the same subject unless this README is split by
8
+explicit decision.
8 9
 
9
-## Quick Start
10
+## Read This First
11
+
12
+This repository manages SSH access from Bogdan's macOS workstation to Next-Gen
13
+company hosts through:
14
+
15
+```text
16
+local macOS
17
+  -> is-jumper 192.168.2.100
18
+  -> J1/J2 10.253.51.50/52:25904
19
+  -> final hosts: porta, pbx, radius, voip, network gear
20
+```
21
+
22
+The key detail agents keep missing:
23
+
24
+- The local machine does not hold the company hardware key.
25
+- The physical RSA smartcard is mounted only on `is-jumper`.
26
+- The wrapper logs into `is-jumper`, sets
27
+  `SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh`, then runs SSH from there to
28
+  J1/J2.
29
+- J1/J2 must use user `bogdan.timofte`.
30
+- `is-jumper` itself must use local key `~/.ssh/keys/is-jumper_ed25519`.
31
+- `ssh` on macOS must resolve to `~/.local/bin/ssh`, not `/usr/bin/ssh`, for
32
+  company aliases.
33
+
34
+Fast health check:
35
+
36
+```bash
37
+which ssh
38
+ssh -G is-jumper | grep -E '^(hostname|user|identityfile|identitiesonly) '
39
+ssh -G j1 | grep -E '^(hostname|user|port) '
40
+ssh is-jumper hostname
41
+ssh porta-sip hostname
42
+```
43
+
44
+Expected highlights:
45
+
46
+```text
47
+/Users/bogdan/.local/bin/ssh
48
+identityfile ~/.ssh/keys/is-jumper_ed25519
49
+user bogdan.timofte
50
+p12.voip.ro
51
+```
52
+
53
+## Repository Rules
54
+
55
+Project source:
56
+
57
+```text
58
+/Users/bogdan/Documents/Workspaces/Bogdan/ssh-infrastructure
59
+```
60
+
61
+Runtime OpenSSH state:
62
+
63
+```text
64
+~/.ssh/config
65
+~/.ssh/known_hosts
66
+~/.ssh/authorized_keys
67
+~/.ssh/keys/
68
+~/.local/bin/ssh
69
+~/.local/bin/scp
70
+~/.local/bin/sftp
71
+```
72
+
73
+Only edit source files in the repository. Do not edit generated runtime files by
74
+hand.
75
+
76
+Tracked source files:
77
+
78
+```text
79
+README.md                         this file, the only documentation
80
+inventory/hosts.yaml              upstream/company host inventory
81
+inventory/hosts-local.yaml        local overlay and local lab inventory
82
+schema/hosts.schema.json          inventory schema
83
+scripts/ssh-wrapper.sh            installed as ~/.local/bin/ssh
84
+scripts/scp-wrapper.sh            installed as ~/.local/bin/scp
85
+scripts/sftp-wrapper.sh           installed as ~/.local/bin/sftp
86
+tools/generate-configs.py         config generator
87
+tools/deploy-local.sh             local deploy
88
+tools/sync-hosts-from-upstream.sh upstream inventory sync
89
+tools/migrate-modern-key.sh       legacy local key migration helper
90
+.gitignore
91
+```
92
+
93
+Ignored or runtime-only files:
94
+
95
+```text
96
+generated/
97
+SSH_SETUP_SUMMARY.md
98
+authorized_keys
99
+known_hosts
100
+known_hosts.old
101
+keys/
102
+agent/
103
+conf.d/
104
+import/
105
+*.pem *.key *.ppk *.der *.csr
106
+```
107
+
108
+Git basics:
109
+
110
+```bash
111
+git status
112
+git add README.md inventory schema scripts tools .gitignore
113
+git commit -m "Describe change"
114
+```
115
+
116
+Known remotes:
117
+
118
+```text
119
+nextgen  ssh://git@192.168.2.103/home/git/repositories/bogdan/NextGen-Host-List.git
120
+mazeri   ssh://git@192.168.2.102/home/git/repositories/bogdan/SSH-Infrastructure.git
121
+```
122
+
123
+## Architecture
124
+
125
+### Network
126
+
127
+```text
128
+192.168.2.0/24 - local office/lab network
129
+  is-jumper 192.168.2.100 - VPN client and hardware-key guardian
130
+  local lab hosts
131
+
132
+10.253.51.0/24 - internal company network reached from is-jumper VPN
133
+  J1 10.253.51.50:25904
134
+  J2 10.253.51.52:25904
135
+  final hosts
136
+```
137
+
138
+`is-jumper` is not a VPN server. It is a local host that has VPN reachability to
139
+the company network and has the physical smartcard mounted.
140
+
141
+### Access Chains
142
+
143
+Standard final-host chain:
144
+
145
+```text
146
+local wrapper
147
+  -> /usr/bin/ssh is-jumper
148
+  -> SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh ssh -A J1
149
+  -> ssh final-host
150
+```
151
+
152
+Interactive J1/J2 login:
153
+
154
+```text
155
+local wrapper -> is-jumper -> J1/J2
156
+```
157
+
158
+Emergency public routes:
159
+
160
+```text
161
+local wrapper -> is-jumper -> j1.next-gen.ro or j2.next-gen.ro
162
+```
163
+
164
+The wrapper strips custom flags before calling real SSH:
165
+
166
+```text
167
+-J1  use J1 VPN route, default
168
+-J2  use J2 VPN route
169
+-j1  use public j1 route
170
+-j2  use public j2 route
171
+```
172
+
173
+Do not reintroduce local port forwarding, Python relays, `IdentityAgent
174
+/tmp/...`, or helper scripts that bridge the physical-card socket to the local
175
+machine. Those were removed for compliance and SentinelOne noise.
176
+
177
+## Keys
178
+
179
+### Key Matrix
180
+
181
+| Key | Location | Purpose |
182
+| --- | --- | --- |
183
+| Physical smartcard RSA 4096 | only on `is-jumper` | Auth from `is-jumper` to J1/J2/company network |
184
+| `is-jumper_ed25519` | local `~/.ssh/keys/is-jumper_ed25519` | Auth from macOS to `is-jumper` |
185
+| Modern ED25519 | local `~/.ssh/id_ed25519` or `~/.ssh/keys/id_ed25519` | Local lab and migrated hosts |
186
+| Legacy RSA | local `~/.ssh/keys/id_rsa_old` | Temporary migration fallback for old local hosts |
187
+
188
+Critical config values:
189
+
190
+```yaml
191
+entrypoints:
192
+  is_jumper:
193
+    hostname: 192.168.2.100
194
+    user: root
195
+    identity_file: ~/.ssh/keys/is-jumper_ed25519
196
+    identities_only: true
197
+
198
+jumps:
199
+  j1:
200
+    hostname: 10.253.51.50
201
+    user: bogdan.timofte
202
+    port: 25904
203
+  j2:
204
+    hostname: 10.253.51.52
205
+    user: bogdan.timofte
206
+    port: 25904
207
+```
208
+
209
+If J1/J2 use `bogdan` instead of `bogdan.timofte`, final host SSH will fail with
210
+an error like:
211
+
212
+```text
213
+bogdan@10.253.51.50: Permission denied (publickey).
214
+Connection to 192.168.2.100 closed.
215
+```
216
+
217
+Fix that in `inventory/hosts-local.yaml`, deploy, then verify:
10 218
 
11 219
 ```bash
12
-# 1. Access the local jump gateway
13
-ssh is-jumper
220
+ssh -G j1 | grep -E '^(hostname|user|port) '
221
+tools/deploy-local.sh
222
+ssh porta-sip hostname
223
+```
224
+
225
+## Inventory and Generation
14 226
 
15
-# 2. From there, reach company network via J1 or J2
16
-ssh j1              # or: ssh j2
227
+The generator reads:
17 228
 
18
-# 3. From J1, reach final hosts
19
-ssh porta-db        # automatic routing: local → is-jumper → J1 → porta-db
229
+```text
230
+inventory/hosts.yaml
231
+inventory/hosts-local.yaml if it exists
20 232
 ```
21 233
 
22
-The `~/.ssh/config` is auto-generated from `inventory/hosts.yaml` — edit the
23
-inventory, not the config file.
234
+Important: the inventory merge is shallow. Later top-level maps from
235
+`hosts-local.yaml` override upstream maps. This is useful for local lab entries
236
+but dangerous for defaults. If `hosts-local.yaml` changes `defaults.jump.user`,
237
+then local `jumps.j1` and `jumps.j2` must specify `user: bogdan.timofte`
238
+explicitly.
24 239
 
25
-## File Structure
240
+Generated files:
26 241
 
27
-- **Project source**: `~/Documents/Workspaces/Bogdan/ssh-infrastructure`
28
-- **OpenSSH runtime**: `~/.ssh` (do not commit)
242
+```text
243
+generated/client.conf      installed as ~/.ssh/config
244
+generated/is-jumper.conf   server-side helper config
245
+generated/j1.conf          server-side final-host config
246
+generated/j2.conf          server-side final-host config
247
+```
29 248
 
30
-Keep secrets and machine-local state out of version control:
249
+`generated/` is ignored by git. Recreate it any time:
31 250
 
32
-- private keys: `~/.ssh/keys/` (not in git)
33
-- `authorized_keys`, `known_hosts`, socket state (not in git)
251
+```bash
252
+python3 tools/generate-configs.py
253
+```
34 254
 
35
-Deploy the local runtime with:
255
+Deploy local runtime:
36 256
 
37 257
 ```bash
38
-# Regenerate ~/.ssh/config from inventory + install wrappers
39 258
 tools/deploy-local.sh
40 259
 ```
41 260
 
42
-## Version control
261
+Deploy does:
43 262
 
44
-This directory is the git repository for source files only. Generated configs,
45
-local state, keys, known hosts, and handoff notes stay out of version control.
263
+```text
264
+1. run tools/generate-configs.py
265
+2. install generated/client.conf as ~/.ssh/config
266
+3. install scripts/ssh-wrapper.sh as ~/.local/bin/ssh
267
+4. install scripts/scp-wrapper.sh as ~/.local/bin/scp
268
+5. install scripts/sftp-wrapper.sh as ~/.local/bin/sftp
269
+6. remove obsolete ~/.ssh/scripts wrapper copies
270
+```
46 271
 
47
-Track source changes with:
272
+It does not touch private keys, `authorized_keys`, or `known_hosts`.
273
+
274
+## Local Shell and Wrappers
275
+
276
+For company aliases, `ssh` must be the wrapper:
48 277
 
49 278
 ```bash
50
-git status
51
-git add inventory schema scripts tools .doc README.md .gitignore
52
-git commit
279
+which ssh
280
+# /Users/bogdan/.local/bin/ssh
281
+```
282
+
283
+If it shows `/usr/bin/ssh`, fix shell PATH and reload:
284
+
285
+```bash
286
+source ~/.zshrc
287
+which ssh
53 288
 ```
54 289
 
55
-## SSH Key Management
290
+The current shell startup should keep `~/.local/bin` first in both interactive
291
+and login shells. If editing these files, preserve this behavior:
56 292
 
57
-Which key goes where? See [KEYS_AND_ACCESS.md](KEYS_AND_ACCESS.md) for the full matrix.
293
+```zsh
294
+path=("$HOME/.local/bin" ${path:#"$HOME/.local/bin"})
295
+export PATH
296
+```
297
+
298
+`ssh-wrapper.sh` uses bash 3.2 compatible array expansion under `set -u`.
299
+Do not replace guarded forms like:
300
+
301
+```bash
302
+${cmd_args[@]+"${cmd_args[@]}"}
303
+```
304
+
305
+with plain:
306
+
307
+```bash
308
+"${cmd_args[@]}"
309
+```
310
+
311
+On macOS bash 3.2, empty arrays plus `set -u` can fail with:
312
+
313
+```text
314
+cmd_args[@]: unbound variable
315
+```
316
+
317
+## Sync from Upstream
58 318
 
59
-Migrate hosts from legacy RSA to modern ED25519:
319
+Pull upstream `hosts.yaml`, apply the local `is-jumper` key override, validate
320
+generation, and deploy if changed:
321
+
322
+```bash
323
+tools/sync-hosts-from-upstream.sh
324
+```
325
+
326
+Defaults:
327
+
328
+```text
329
+UPSTREAM_SSH_TARGET=nextgen@192.168.2.103
330
+UPSTREAM_HOSTS_PATH=/home/nextgen/projects/ssh-infrastructure/inventory/hosts.yaml
331
+LOCAL_IS_JUMPER_IDENTITY_FILE=~/.ssh/keys/is-jumper_ed25519
332
+DEPLOY_AFTER_SYNC=1
333
+FORCE_DEPLOY=0
334
+```
335
+
336
+Useful overrides:
337
+
338
+```bash
339
+UPSTREAM_HOSTS_FILE=/tmp/hosts.yaml tools/sync-hosts-from-upstream.sh
340
+DEPLOY_AFTER_SYNC=0 tools/sync-hosts-from-upstream.sh
341
+FORCE_DEPLOY=1 tools/sync-hosts-from-upstream.sh
342
+UPSTREAM_SSH_TARGET=user@host tools/sync-hosts-from-upstream.sh
343
+```
344
+
345
+After sync, always check J1 user because the local overlay can override jump
346
+defaults:
347
+
348
+```bash
349
+ssh -G j1 | grep -E '^(hostname|user|port) '
350
+```
351
+
352
+Expected:
353
+
354
+```text
355
+user bogdan.timofte
356
+hostname 10.253.51.50
357
+port 25904
358
+```
359
+
360
+## Adding or Changing Hosts
361
+
362
+For company/Next-Gen hosts:
363
+
364
+```text
365
+1. Edit inventory/hosts.yaml or sync it from upstream.
366
+2. Keep local-only corrections in inventory/hosts-local.yaml.
367
+3. Run tools/deploy-local.sh.
368
+4. Verify with ssh -G <alias>.
369
+5. Verify read-only with ssh <alias> hostname.
370
+6. Commit source changes only.
371
+```
372
+
373
+For local lab hosts:
374
+
375
+```text
376
+1. Edit inventory/hosts-local.yaml.
377
+2. Run tools/deploy-local.sh.
378
+3. Verify with ssh <alias> hostname.
379
+4. Commit the local overlay change.
380
+```
381
+
382
+Common inventory defaults:
383
+
384
+| Context | User | Port |
385
+| --- | --- | --- |
386
+| J1/J2 company jump | `bogdan.timofte` | `25904` for VPN route |
387
+| Company final hosts | usually `bogdan` | usually `22` |
388
+| Company inherited jump config | `bogdan.timofte` | often `24` |
389
+| Local lab hosts | usually `bogdan` | usually `22` |
390
+| Cisco/OLT interactive devices | inventory-specific | `22` |
391
+
392
+For Cisco/OLT/password-interactive devices, set:
393
+
394
+```yaml
395
+auth: password_interactive
396
+```
397
+
398
+The wrapper then avoids forcing `BatchMode=yes` and disables pubkey auth for
399
+that final hop.
400
+
401
+## Key Migration for Local Legacy Hosts
402
+
403
+Modern preferred key:
404
+
405
+```text
406
+~/.ssh/id_ed25519.pub
407
+```
408
+
409
+Legacy fallback key:
410
+
411
+```text
412
+~/.ssh/keys/id_rsa_old
413
+```
414
+
415
+Migrate all configured local legacy hosts:
60 416
 
61 417
 ```bash
62
-# Migrate all legacy hosts
63 418
 tools/migrate-modern-key.sh
419
+```
420
+
421
+Migrate one host:
64 422
 
65
-# Migrate a specific host
423
+```bash
66 424
 tools/migrate-modern-key.sh is-baobab
67 425
 ```
68 426
 
69
-Details: [docs/KEY_MIGRATION.md](docs/KEY_MIGRATION.md)
427
+Manual fallback if password access is available:
70 428
 
71
-## Current client layout
429
+```bash
430
+ssh -o PubkeyAuthentication=no user@host \
431
+  "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" \
432
+  < ~/.ssh/id_ed25519.pub
433
+```
434
+
435
+Keep `id_rsa_old` until all legacy hosts are verified with the modern key.
436
+
437
+## Verification Checklist
438
+
439
+Run after deploy, sync, wrapper edits, or inventory changes:
440
+
441
+```bash
442
+which ssh
443
+ssh -G is-jumper | grep -E '^(hostname|user|identityfile|identitiesonly) '
444
+ssh -G j1 | grep -E '^(hostname|user|port) '
445
+ssh is-jumper hostname
446
+ssh is-jumper 'SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh ssh-add -L | sed -n 1p'
447
+ssh porta-sip hostname
448
+ssh pbx-bo hostname
449
+```
450
+
451
+Expected signals:
72 452
 
73 453
 ```text
74
-~/.ssh/config
75
-~/.local/bin/ssh
76
-~/.local/bin/scp
77
-~/.local/bin/sftp
454
+which ssh                         -> /Users/bogdan/.local/bin/ssh
455
+is-jumper hostname                -> is-vpn-gw
456
+j1 user                           -> bogdan.timofte
457
+physical card check               -> ssh-rsa ... cardno:6446168
458
+porta-sip hostname                -> p12.voip.ro
459
+pbx-bo hostname                   -> pbx-bo
78 460
 ```
79 461
 
80
-The wrapper sources stay versioned in `scripts/` inside the project; deploy
81
-installs executable copies into `~/.local/bin` and removes the obsolete
82
-`~/.ssh/scripts` runtime layout from older checkouts.
462
+Interactive smoke test:
463
+
464
+```bash
465
+printf "exit\n" | ssh porta-sip
466
+printf "exit\n" | ssh pbx-bo
467
+```
468
+
469
+## Troubleshooting
470
+
471
+### `bogdan@10.253.51.50: Permission denied (publickey)`
472
+
473
+The wrapper reached `is-jumper`, but J1 was attempted with user `bogdan`.
474
+J1/J2 need `bogdan.timofte`.
475
+
476
+Check:
477
+
478
+```bash
479
+ssh -G j1 | grep -E '^(hostname|user|port) '
480
+```
481
+
482
+Fix:
483
+
484
+```yaml
485
+# inventory/hosts-local.yaml
486
+jumps:
487
+  j1:
488
+    user: bogdan.timofte
489
+  j2:
490
+    user: bogdan.timofte
491
+```
492
+
493
+Deploy:
494
+
495
+```bash
496
+tools/deploy-local.sh
497
+ssh porta-sip hostname
498
+```
83 499
 
84
-## Source of Truth
500
+### `root@192.168.2.100: Permission denied`
85 501
 
86
-The structured source of truth starts in:
502
+The local connection to `is-jumper` is using the wrong key.
503
+
504
+Check:
505
+
506
+```bash
507
+ssh -G is-jumper | grep -E '^(user|hostname|identityfile|identitiesonly) '
508
+ls -l ~/.ssh/keys/is-jumper_ed25519
509
+```
510
+
511
+Expected:
87 512
 
88 513
 ```text
89
-inventory/hosts.yaml
90
-schema/hosts.schema.json
91
-tools/generate-configs.py
514
+user root
515
+hostname 192.168.2.100
516
+identityfile ~/.ssh/keys/is-jumper_ed25519
517
+identitiesonly yes
518
+```
519
+
520
+If generated config is wrong, fix `inventory/hosts-local.yaml` or
521
+`inventory/hosts.yaml`, then deploy.
522
+
523
+### `ssh pbx-bo` uses `/usr/bin/ssh`
524
+
525
+The wrapper is not first in PATH.
526
+
527
+Check:
528
+
529
+```bash
530
+which ssh
92 531
 ```
93 532
 
94
-The `generated/*.conf` files are deploy artifacts. They are ignored by git and
95
-can be recreated at any time with `tools/deploy-local.sh`.
533
+Fix current shell:
534
+
535
+```bash
536
+source ~/.zshrc
537
+```
96 538
 
97
-## Inventory and Config Generation
539
+If needed, ensure `.zprofile` and `.zshrc` both move `~/.local/bin` to the front
540
+using zsh `path`, not a guard that leaves it later in PATH.
98 541
 
99
-The single source of truth:
542
+### `cmd_args[@]: unbound variable`
543
+
544
+This is a bash 3.2 plus `set -u` empty-array issue in `ssh-wrapper.sh`.
545
+
546
+Use guarded array expansion:
547
+
548
+```bash
549
+${array[@]+"${array[@]}"}
550
+```
551
+
552
+Do not simplify it.
553
+
554
+### Physical card missing on `is-jumper`
555
+
556
+Check:
557
+
558
+```bash
559
+ssh is-jumper 'ls -l /run/user/0/gnupg/S.gpg-agent.ssh'
560
+ssh is-jumper 'SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh ssh-add -L | sed -n 1p'
561
+```
562
+
563
+Expected key output contains:
100 564
 
101 565
 ```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
566
+cardno:6446168
108 567
 ```
109 568
 
110
-### User and Port Defaults
569
+If missing, the issue is on `is-jumper`: gpg-agent, card mount, permissions, or
570
+hardware state.
111 571
 
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` |
572
+### Direct command works but wrapper fails
117 573
 
118
-### Deployment
574
+Compare generated command behavior:
575
+
576
+```bash
577
+bash -x ~/.local/bin/ssh porta-sip hostname
578
+```
579
+
580
+Look for:
581
+
582
+```text
583
+SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh
584
+bogdan.timofte@10.253.51.50
585
+```
586
+
587
+If either is wrong, fix inventory/local overlay or wrapper.
588
+
589
+### Generated config was edited manually
590
+
591
+Discard manual runtime edits by redeploying:
119 592
 
120 593
 ```bash
121
-# Option 1: Full deploy (recommended)
122 594
 tools/deploy-local.sh
595
+```
596
+
597
+Then verify:
598
+
599
+```bash
600
+ssh -G j1 | grep -E '^(hostname|user|port) '
601
+```
602
+
603
+## Compatibility and Compliance
604
+
605
+Do not reintroduce these removed patterns:
606
+
607
+```text
608
+j1-relay.sh
609
+ssh-proxy.sh
610
+ensure-ssh-agent-bridge.sh
611
+ensure-ssh-jump.sh
612
+local socket forwarding for the hardware card
613
+Python/base64 port-forwarding relays
614
+per-host local ProxyCommand bridges
615
+```
123 616
 
124
-# Option 2: Manual steps
125
-python3 tools/generate-configs.py           # Regenerate configs
126
-cp generated/client.conf ~/.ssh/config      # Install client config
617
+Current compliant model:
618
+
619
+```text
620
+local wrapper -> ssh is-jumper -> run normal ssh from is-jumper
127 621
 ```
128 622
 
129
-### Sync from Upstream
623
+Compatibility options for old final hosts belong in inventory or on jump hosts,
624
+not in ad-hoc local forwarding scripts.
130 625
 
131
-Pull latest `hosts.yaml` from nextgen and redeploy:
626
+## Maintenance Notes for Agents
627
+
628
+Before changing anything:
132 629
 
133 630
 ```bash
134
-tools/sync-hosts-from-upstream.sh
631
+git status --short --branch
632
+which ssh
633
+ssh -G j1 | grep -E '^(hostname|user|port) '
634
+```
635
+
636
+When fixing auth:
135 637
 
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
638
+```text
639
+1. Identify which hop failed from the error user@host.
640
+2. is-jumper failures mean local key/config.
641
+3. J1/J2 failures mean hardware card, SSH_AUTH_SOCK, or jump user.
642
+4. final-host failures mean final host user/auth/port.
643
+5. Apply the fix in inventory or wrapper source, not generated config.
644
+6. Run tools/deploy-local.sh.
645
+7. Run read-only SSH verification.
646
+8. Commit the source change.
647
+```
648
+
649
+Do not assume `hosts.yaml` alone is the effective config. Always remember
650
+`inventory/hosts-local.yaml` is merged in by `tools/generate-configs.py`.
651
+
652
+Do not trust stale docs, comments, or generated files over these commands:
653
+
654
+```bash
655
+ssh -G <alias>
656
+tools/deploy-local.sh
657
+ssh <alias> hostname
658
+git diff
139 659
 ```
+0 -126
docs/KEY_MIGRATION.md
@@ -1,126 +0,0 @@
1
-# SSH Key Migration: Legacy to Modern
2
-
3
-## Overview
4
-
5
-This document describes the process for migrating hosts from legacy SSH keys (`id_rsa_old`) to modern keys (`id_ed25519`).
6
-
7
-## Rationale
8
-
9
-- **Legacy key** (`id_rsa_old` from 2015): RSA 2048-bit, older algorithm support
10
-- **Modern key** (`id_ed25519`): EdDSA 256-bit, better security, smaller key size, faster
11
-
12
-## Automatic Migration
13
-
14
-### Migrate all legacy hosts
15
-
16
-```bash
17
-tools/migrate-modern-key.sh
18
-```
19
-
20
-This will:
21
-1. Check all hosts in `inventory/hosts-local.yaml` legacy_infrastructure group
22
-2. Test if modern key already works
23
-3. Use legacy key to install modern key if needed
24
-4. Verify modern key works after installation
25
-
26
-### Migrate specific host
27
-
28
-```bash
29
-tools/migrate-modern-key.sh is-baobab
30
-```
31
-
32
-## Manual Migration (if script fails)
33
-
34
-For a specific host, e.g., `is-nasturel`:
35
-
36
-### 1. Interactive password auth
37
-
38
-If password is available:
39
-
40
-```bash
41
-ssh -o PubkeyAuthentication=no sshd@192.168.2.144 \
42
-  "mkdir -p ~/.ssh && \
43
-   echo '$(cat ~/.ssh/id_ed25519.pub)' >> ~/.ssh/authorized_keys && \
44
-   chmod 600 ~/.ssh/authorized_keys"
45
-```
46
-
47
-### 2. Through jump host (is-jumper)
48
-
49
-If accessible through is-jumper:
50
-
51
-```bash
52
-ssh is-jumper \
53
-  "ssh -o StrictHostKeyChecking=accept-new sshd@192.168.2.144 \
54
-    'mkdir -p ~/.ssh && echo \$(cat ~/.ssh/id_ed25519.pub) >> ~/.ssh/authorized_keys'"
55
-```
56
-
57
-### 3. Physical/console access
58
-
59
-If all else fails, use console/IPMI access to manually:
60
-- Edit `/home/sshd/.ssh/authorized_keys`
61
-- Add modern key: `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...`
62
-
63
-## Adding New Hosts
64
-
65
-When adding new legacy hosts to the inventory:
66
-
67
-1. **In `inventory/hosts-local.yaml`**: add host to `legacy_infrastructure` group
68
-2. **Run deployment**: `tools/deploy-local.sh`
69
-3. **Run migration**: `tools/migrate-modern-key.sh <new-host>`
70
-4. **Commit**: `git add . && git commit -m "Add/migrate <host>"`
71
-
72
-## Status Tracking
73
-
74
-Check which hosts have been migrated:
75
-
76
-```bash
77
-# Modern key working
78
-for h in $(grep -oE "is-[a-z0-9-]+" inventory/hosts-local.yaml | sort -u); do
79
-  timeout 1 ssh -o BatchMode=yes "$h" true 2>/dev/null && echo "$h: ✓" || echo "$h: ⚠"
80
-done
81
-```
82
-
83
-## Key Files
84
-
85
-- **Modern (preferred)**: `~/.ssh/id_ed25519`
86
-- **Legacy (deprecated)**: `~/.ssh/keys/id_rsa_old`
87
-
88
-Both should be kept until all hosts are migrated.
89
-
90
-## Removing Legacy Key
91
-
92
-Once all hosts migrated and verified:
93
-
94
-```bash
95
-# Backup first
96
-cp ~/.ssh/keys/id_rsa_old ~/.ssh/keys/id_rsa_old.backup
97
-
98
-# Remove from SSH config (if any explicit config)
99
-grep -r "id_rsa_old" . && echo "Still in use" || echo "Safe to remove"
100
-
101
-# Safe to delete after backup
102
-rm ~/.ssh/keys/id_rsa_old
103
-```
104
-
105
-## Troubleshooting
106
-
107
-### "Permission denied (publickey)" with modern key
108
-
109
-→ Host still using legacy auth only. Use `tools/migrate-modern-key.sh <host>`
110
-
111
-### Legacy key also fails to connect
112
-
113
-→ Host might be offline or firewall blocked. Check connectivity first:
114
-
115
-```bash
116
-ping 192.168.2.91  # Replace with host IP
117
-```
118
-
119
-### SSH hangs or times out
120
-
121
-→ Reduce timeout, check firewall rules. Use `-o ConnectTimeout=2` flag.
122
-
123
-## References
124
-
125
-- [Ed25519 SSH keys - OpenSSH](https://man.openbsd.org/ssh-keygen)
126
-- [Why Ed25519 is superior to RSA](https://ianix.com/pub/ed25519-deployment.html)