# Arhitectura SSH Curenta

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.

## Structura Locala

```text
/home/nextgen/.ssh/
  config                        <- single-file generat din inventory/hosts.yaml
  known_hosts
  known_hosts.old
  authorized_keys
  keys/
    id_ed25519
    id_ed25519.pub
    elastix-root.pub
  .doc/
    ssh-jump-architecture.md

~/.local/bin/
  ssh
  scp
  sftp
```

Reguli:

- Proiectul versionat sta in `~/projects/ssh-infrastructure`; `~/.ssh` ramane runtime OpenSSH.
- Cheile de identitate stau in `~/.ssh/keys`.
- Hosturile SSH sunt definite in `inventory/hosts.yaml`, apoi generate in `generated/*.conf`.
- `~/.ssh/config` este instalat din `generated/client.conf` prin `tools/deploy-local.sh`; nu se editeaza manual.
- Scripturile sursa stau versionate in `scripts/` in repo.
- `~/.local/bin/` contine copiile executabile instalate de `tools/deploy-local.sh`; nu se editeaza manual.
- `authorized_keys`, `known_hosts` si `config` raman in radacina `.ssh`, pentru compatibilitate cu OpenSSH.
- `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.

## Wrappers locale

Scripturile sursa sunt in `scripts/` in repo; `~/.local/bin/` contine copiile executabile instalate local.

| Comanda | Script | Mecanism |
| --- | --- | --- |
| `ssh` | `ssh-wrapper.sh` | parseaza args, rezolva hostul via `ssh -G`, construieste sesiunea nested |
| `scp` | `scp-wrapper.sh` | apeleaza `/usr/bin/scp -S ~/.local/bin/ssh` — rutarea e delegata wrapper-ului ssh |
| `sftp` | `sftp-wrapper.sh` | apeleaza `/usr/bin/sftp -S ~/.local/bin/ssh` — idem |

- Compatibilitatea SSH pentru hop-ul final este responsabilitatea serverelor `J1`/`J2`; wrapperele locale nu reproduc algoritmi, KEX, cipher-uri sau `OPENSSL_CONF`.
- Daca PATH-ul include `~/.local/bin` inaintea `/usr/bin`, comenzile `ssh`/`scp`/`sftp` folosesc automat aceste wrappers.

## Arhitectura rețelei

```
192.168.2.0/24  — rețea locală de birou
  is-toltec  (workstation-ul local)
  is-jumper  192.168.2.100  (gardian cheie + client VPN)

10.253.51.0/24  — rețea internă companie (acces via VPN de pe is-jumper)
  J1  10.253.51.50:25904
  J2  10.253.51.52:25904
  hosturi finale (voip, porta, radius etc.)
```

**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`.

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

## Lanțul de acces

Calea standard (VPN activ, J1):

```text
local
  -> is-jumper (192.168.2.100, executor SSH + cheie fizica)
  -> J1 (10.253.51.50:25904)
  -> host final
```

Calea publică (urgențe, fără rută VPN, j1/j2):

```text
local
  -> is-jumper (192.168.2.100, executor SSH + cheie fizica)
  -> j1.next-gen.ro:25904
  -> host final
```

Sesiuni interactive pe J1/J2:

```text
local -> is-jumper -> J1
local -> is-jumper -> J2
```

Nu mai exista bridge local de agent (`/tmp/is-jumper-agent.sock`). Cheia fizica ramane folosita local pe `is-jumper`.

Jumps disponibile via flaguri wrapper:

| Flag | Jump | Rută | Când |
| --- | --- | --- | --- |
| implicit / `-J1` | J1 (10.253.51.50) | client SSH rulat pe is-jumper | standard |
| `-J2` | J2 (10.253.51.52) | client SSH rulat pe is-jumper | failover VPN |
| `-j1` | j1.next-gen.ro | client SSH rulat pe is-jumper | urgențe |
| `-j2` | j2.next-gen.ro | client SSH rulat pe is-jumper | urgențe |

## Scripturi

### ssh-wrapper.sh

Parseaza argumentele `ssh`, rezolva hostul via `ssh -G`, construieste sesiunea nested activa pe `is-jumper`:

```
exec /usr/bin/ssh is-jumper "SSH_AUTH_SOCK=/run/user/0/gnupg/S.gpg-agent.ssh ssh -A <jump_host> '<final_ssh_cmd>'"
```

Bypass automat (pass-through direct) pentru:
- `is-jumper` / `192.168.2.100`
- Flaguri de interogare: `-G`, `-Q`, `-V`, `--help`

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

### scp-wrapper.sh / sftp-wrapper.sh

Wrappers subtiri care injecteaza `-S ~/.local/bin/ssh` la apelul sistemului:

```bash
/usr/bin/scp -S ~/.local/bin/ssh "$@"
/usr/bin/sftp -S ~/.local/bin/ssh "$@"
```

Rutarea este delegata integral catre `ssh-wrapper.sh`; `scp`/`sftp` raman transparente pentru utilizator.

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.

## Config SSH

`~/.ssh/config` este un single-file generat din `inventory/hosts.yaml`:

```sshconfig
# Generated by tools/generate-configs.py.
# Do not edit this file directly; edit inventory/hosts.yaml.
```

Generatorul produce:

| Fisier | Destinatie | Rol |
| --- | --- | --- |
| `generated/client.conf` | client local | config runtime pentru `~/.ssh/config` |
| `generated/is-jumper.conf` | is-jumper | config pentru jump aliases |
| `generated/j1.conf` | J1 | hosturi finale |
| `generated/j2.conf` | J2 | hosturi finale |

Reguli de configurare:

- hosturile finale definesc doar `HostName`, `User` si `Port`;
- pe client, hosturile importate din IFS folosesc drept `HostName` numele
  canonic deja cunoscut pe jump, iar IP-ul ramane alias pentru autocomplete;
- `is-jumper` defineste si cheia locala de acces (`IdentityFile`, `IdentitiesOnly`);
- wrapperul construieste ruta nested prin `is-jumper -> J1` implicit;
- optiunile comune per-grup (ConnectTimeout etc.) stau in `inventory/hosts.yaml`;
- `generated/j1.conf` si `generated/j2.conf` mostenesc blocul global company-managed
  si omit `User`/`Port` cand toate aliasurile unui host sunt deja acoperite de
  regulile `Match Host` de pe jump;
- `generated/j1.conf` si `generated/j2.conf` nu mai includ hosturile care folosesc
  doar defaulturi mostenite; raman doar override-urile efective si exceptiile
  per-pattern care trebuie pastrate pe jump;
- `generated/j1.conf` si `generated/j2.conf` sunt generate fara comentarii,
  doar cu stanza-urile SSH necesare;
- hosturile Radius folosesc acelasi flux nested prin J1 ca restul hosturilor finale.

Aliasuri principale:

| Alias | Rol | Rutare |
| --- | --- | --- |
| `is-jumper` | VPN gateway + gardian cheie fizica | direct (IP 192.168.2.100) |
| `J1`, `J2` | jump hosts, login interactiv | wrapper prin is-jumper |
| `voip-prov`, `voip-prov-root` | host VoIP provider | wrapper nested prin J1 |
| `porta-sip`, `porta-web`, `porta-db`, `porta-configurator` | PortaOne MR30 | wrapper nested prin J1 |
| `elastix` | host vechi | wrapper nested prin J1 |
| `*.radius-db`, `*.radius-pppoe` | baze Radius regionale | wrapper nested prin J1 |

`J1` si `J2` nu mai folosesc `ProxyJump`, `IdentityAgent` sau `RemoteCommand` in configul local; wrapper-ul orchestreaza explicit conexiunea prin `is-jumper`.

## PortaOne

Aliasuri PortaOne:

- `porta-sip`, `p12-sip`, `p12`, `p12.voip.ro` -> `193.16.148.4`
- `porta-web`, `porta-api`, `porta-slave`, `porta7`, `telefonie.next-gen.ro` -> `193.16.148.7`
- `porta-db`, `porta-master`, `porta1` -> `193.16.148.11`
- `porta-configurator`, `porta-config` -> `193.16.148.13`

Aceste hosturi folosesc ruta nested din wrapper; eventualele optiuni de compatibilitate ale hop-ului final sunt asigurate pe `J1`/`J2`.

## Verificari

Verificare configuratie fara conectare:

```bash
ssh -G is-jumper | grep -E '^(hostname|user|identityfile|identitiesonly)'
ssh -G porta-db | grep -E '^(hostname|user|port)'
ssh -G falticeni.radius-db | grep -E '^(hostname|user|port|connecttimeout)'
```

Verificare read-only cu conectare:

```bash
ssh porta-db hostname
ssh porta-sip 'service porta-sip status'
ssh voip-prov hostname
scp porta-db:/etc/hosts /tmp/test-scp
sftp porta-db <<< 'ls /'
```

## Mentenanta

- Cand adaugi o cheie noua, pune-o in `~/.ssh/keys` si seteaza permisiuni `600` pentru cheia privata.
- Cand adaugi un host nou: adauga-l in `inventory/hosts.yaml`, ruleaza generatorul si apoi `tools/deploy-local.sh`.
- Cand adaugi un domeniu nou: adauga un grup nou in `inventory/hosts.yaml`.
- Cand adaugi un wrapper nou: adauga scriptul in `scripts/` in repo si lasa `tools/deploy-local.sh` sa il instaleze in `~/.local/bin/`.
- Nu readuce `ProxyJump`, `IdentityAgent /tmp/is-jumper-agent.sock`, helperul Python sau port-forwarding per-host.
- Nu activa `ForwardAgent yes` pe hosturile finale fara un motiv explicit.
- Dupa modificari la wrappers sau config, verifica: `ssh -G <alias>` si cel putin un alias read-only (`ssh porta-db hostname`).
