# SSH tunnels

Persistent SSH tunnel between two VPS with autossh: a receiver service shows up on the initiator's localhost without being exposed.

Source: https://ardumaker.com/en/docs/tuneles-ssh/

- Category: Servers and infrastructure
- Status: Available
- Requirements: Two VPS (from the same company for the automatic flow); Being a company member with access to both VPS; Systems with systemd and a compatible OpenSSH (check with `ver_funciones_vps`, feature “SSH tunnels between VPS”)
- MCP tools (Read only): `ver_estado_receptor_tunel_vps`, `listar_claves_receptor_tunel_vps`, `ver_estado_iniciador_tunel_vps`, `listar_tuneles_vps`, `ver_tunel_vps`
- MCP tools (Write or modify): `configurar_receptor_tunel_vps`, `agregar_clave_receptor_tunel_vps`, `configurar_iniciador_tunel_vps`, `crear_tunel_vps`, `controlar_tunel_vps`
- MCP tools (Destructive): `configurar_tunel_entre_vps`, `eliminar_receptor_tunel_vps`, `eliminar_clave_receptor_tunel_vps`, `eliminar_iniciador_tunel_vps`, `eliminar_tunel_vps`

## What can you do with it?

- Use a service listening on one VPS (a database, an internal API) from another VPS as if it were on `localhost`, without publishing it on the internet.
- Set everything up in a single call (`configurar_tunel_entre_vps`), or step by step.
- Authorize or revoke the keys allowed into the receiver.
- Start, stop or restart each tunnel, and see its live status.

## When should you use it?

- An app on one VPS needs the database living on another.
- Splitting an internal API from its frontend without opening public ports for the service.

## How does it work in ArduMaker?

It is a **local** forward (`ssh -L`) running on the **initiator**: whatever listens on the receiver's `puerto_remoto` becomes available at `localhost:puerto_local` inside the initiator. It does not work the other way round and exposes nothing to the internet.

### The steps

1. **Receiver** (`configurar_receptor_tunel_vps`): creates a user dedicated to the tunnel, configures `sshd` to accept that user on port 2222 (no terminal, no X11, no agent, TCP forwarding only) and publishes 2222 through a port forward. It runs in the background.
2. **Initiator** (`configurar_iniciador_tunel_vps`): installs `autossh` and generates an ed25519 key pair. Also in the background.
3. **Key**: the initiator's public key is authorized on the receiver (`agregar_clave_receptor_tunel_vps`).
4. **Tunnel** (`crear_tunel_vps`): creates on the initiator a systemd service `ardutunnel_<nombre>` running `autossh -L <local>:<target>:<port>` against the receiver, enables it and starts it.

`configurar_tunel_entre_vps` does all four steps and also starts the tunnel. It waits for each role to finish installing (up to a few minutes); if one does not finish in time, it stops with `tunel_rol_instalando` and can be resumed with `desde`. A role already installed is not reinstalled, a key already authorized is reused and a tunnel already created with that name and local port comes back as it is.

## What actually changes?

### On the receiver

- A tunnel user with its authorized keys in its own `authorized_keys` block.
- An ArduMaker-managed `sshd` configuration file that adds port 2222 with its `Match LocalPort` block (port 22 stays declared). Do not edit it by hand: it is fully rewritten whenever a role changes.
- A public port forward to the VPS port 2222.

### On the initiator

- The `autossh` package and an ed25519 key pair for the tunnel user.
- One `ardutunnel_<nombre>.service` per tunnel, with `Restart=always`, enabled at boot.
- A tunnel-specific `known_hosts`: the receiver's first key is pinned and, if it later changes, the tunnel does not connect.

## Ports and connection

| Item | Value |
| --- | --- |
| Receiver port inside the VPS | 2222 |
| Public port | The `host_port` of the forward to 2222 (`public_port`) |
| Host | The receiver `dnsip` |
| Connection string | `<usuario>@<dnsip> -p <public_port>` |

```ini
ExecStart=/usr/bin/autossh -M 0 -N \
  -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" \
  -o "StrictHostKeyChecking=accept-new" -o "ExitOnForwardFailure=yes" \
  -L <puerto_local>:localhost:<puerto_remoto> \
  -i <clave_del_tunel> -p <public_port> <usuario>@<dnsip>
```

`ver_estado_receptor_tunel_vps` returns the connection string and a usage example to connect from another machine with `ssh -N -L`.

## Diagram

**A tunnel between two VPS**

```text
App on the initiator (localhost:<puerto_local>)
  ↓
autossh (ardutunnel_<nombre> service)
  ↓
Receiver port forward (<dnsip>:<public_port> → 2222)
  ↓
Receiver sshd :2222 (tunnel user only, forwarding only)
  ↓
Receiver service (localhost:<puerto_remoto>)
```

## Expected result

- `ver_tunel_vps` shows the tunnel with the live status of its systemd service.
- `listar_tuneles_vps` lists the initiator's tunnels; `ver_estado_iniciador_tunel_vps` returns its public key and the tunnel count.
- From the initiator, the service answers at `localhost:<puerto_local>`.

```bash
systemctl status ardutunnel_mi-base
nc -vz localhost 15432
```

## Errors and troubleshooting

| Answer | Cause | What to do |
| --- | --- | --- |
| 409 `tunel_rol_instalando` | A role is still installing | Ask again; it resumes from there |
| `tunel_rol_fallido` | A role installation failed | Check the role status and retry |
| `tunel_mismo_vps` | Receiver and initiator are the same VPS | Use two different VPS |
| 409 local port in use | Another initiator tunnel already uses that `local_port` | Pick another port |
| 409 `Key already registered` | The key is already authorized | Nothing to do |
| The tunnel does not connect | The receiver key changed, or the initiator key is not authorized | Check `ver_logs_systemd_vps` for the `ardutunnel_<nombre>` service |

> **PRECAUCION:** `eliminar_iniciador_tunel_vps` deletes **all** the VPS tunnels at once, and `eliminar_receptor_tunel_vps` removes the tunnel user and its keys: incoming tunnels stop working. `controlar_tunel_vps` with `stop` drops whatever is going through the tunnel.

If a step of the flow fails, whatever can be undone is undone; installed packages remain and are reported in `residuo` (reinstalling changes nothing). The flow accepts `idempotency_key` and `simular`.

## MCP access

Yes, in the `/mcp` and `/mcp/vps` profiles. Installing a role returns an `operacion_id`: `ver_operacion` says when it is ready.

| Tool | Type |
| --- | --- |
| `configurar_tunel_entre_vps` | Write (full flow) |
| `ver_estado_receptor_tunel_vps`, `listar_claves_receptor_tunel_vps`, `ver_estado_iniciador_tunel_vps`, `listar_tuneles_vps`, `ver_tunel_vps` | Read |
| `configurar_receptor_tunel_vps`, `configurar_iniciador_tunel_vps`, `agregar_clave_receptor_tunel_vps`, `crear_tunel_vps` | Write |
| `controlar_tunel_vps` | Write (`stop` drops traffic) |
| `eliminar_tunel_vps`, `eliminar_clave_receptor_tunel_vps`, `eliminar_receptor_tunel_vps`, `eliminar_iniciador_tunel_vps` | Destructive |

Reference at [/mcp/docs](https://ardumaker.com/en/mcp/docs) and examples at [/mcp/tuneles](https://ardumaker.com/en/mcp/tuneles).

## Related features

- [Firewall and ports](https://ardumaker.com/en/docs/firewall-y-puertos/)
- [systemd services](https://ardumaker.com/en/docs/systemd/)
- [PostgreSQL](https://ardumaker.com/en/docs/postgresql/)
- [VPS servers](https://ardumaker.com/en/docs/vps/)
