|
| 1 | +# Remote access: tunnel, Tailscale and what must come first |
| 2 | + |
| 3 | +*(Versão em português ao final.)* |
| 4 | + |
| 5 | +Reaching the gateway from another machine — your laptop away from home, a |
| 6 | +teammate, a phone — has three usual answers. They differ in who can reach you, |
| 7 | +and the order in which you turn things on decides whether that is safe. |
| 8 | + |
| 9 | +> **This page is about getting IN.** Sending traffic OUT through a chosen |
| 10 | +> address, so each account has its own source IP, is a different problem with a |
| 11 | +> different answer — see [Egress and Multi-Session](Egress-And-Multi-Session). |
| 12 | +> Tailscale appears in both pages doing two unrelated jobs. |
| 13 | +
|
| 14 | +--- |
| 15 | + |
| 16 | +## Read this before enabling anything |
| 17 | + |
| 18 | +The stacks in this repository ship with: |
| 19 | + |
| 20 | +```yaml |
| 21 | +- REQUIRE_API_KEY=false |
| 22 | +- REQUIRE_LOGIN=false |
| 23 | +``` |
| 24 | +
|
| 25 | +That is **safe while the port is bound to `127.0.0.1`**, which is how every |
| 26 | +compose here publishes it: only your own machine can reach it, and demanding a |
| 27 | +password from yourself on localhost adds friction without adding safety. |
| 28 | + |
| 29 | +The moment you expose the gateway, that reasoning inverts. 9Router's dashboard |
| 30 | +says it in a yellow banner, and it is not decoration: |
| 31 | + |
| 32 | +> *Enable "Require login" and set a custom password before activating the tunnel.* |
| 33 | + |
| 34 | +Two things make this sharper than it looks: |
| 35 | + |
| 36 | +1. **`/v1` is a public prefix.** In `src/dashboardGuard.js`, the gateway lists its |
| 37 | + own inference endpoints — `/v1`, `/v1beta`, `/api/v1`, `/codex`, `/responses` — |
| 38 | + as public, because they are meant to be called by your coding tools without a |
| 39 | + dashboard session. With `REQUIRE_API_KEY=false` and the gateway on a public URL, |
| 40 | + **anyone who learns the URL can spend your accounts**. |
| 41 | +2. **The gateway's admin routes fall open too.** Its `/api/settings`, `/api/keys`, |
| 42 | + `/api/providers` and the rest of the gateway's admin surface are protected |
| 43 | + *only when `requireLogin` is on*. |
| 44 | + With it off, an exposed gateway hands over its own configuration. |
| 45 | + |
| 46 | +So the order is not a preference: |
| 47 | + |
| 48 | +``` |
| 49 | +1. REQUIRE_LOGIN=true + a password you chose |
| 50 | +2. REQUIRE_API_KEY=true + a key for your tools |
| 51 | +3. only then, the tunnel or Tailscale |
| 52 | +``` |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Option 1 — Cloudflare tunnel (built into 9Router) |
| 57 | + |
| 58 | +The dashboard's **API Endpoint** screen has a `Tunnel` button. It registers a |
| 59 | +Cloudflare quick tunnel and gives you a public `https://…trycloudflare.com` |
| 60 | +address that reaches your gateway without opening any port on your router. |
| 61 | + |
| 62 | +**When it fits:** you need a URL reachable from anywhere, including devices you |
| 63 | +do not control, and you accept that the address is public to whoever has it. |
| 64 | + |
| 65 | +**What to know:** |
| 66 | + |
| 67 | +- The URL is **public**. There is no allow-list — the only thing between the |
| 68 | + internet and your accounts is `REQUIRE_LOGIN` and `REQUIRE_API_KEY`. |
| 69 | +- The address changes every time the tunnel is re-enabled, unless you bring your |
| 70 | + own named Cloudflare tunnel. |
| 71 | +- The dashboard blocks the button while login is off — but that gate lives in the |
| 72 | + screen. The gateway's own `POST /api/tunnel/enable` does not re-check it, so a script or an |
| 73 | + extension can enable the tunnel while login is still off. Set the two flags |
| 74 | + first and the question does not arise. |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## Option 2 — Tailscale (built into 9Router, and the one to prefer) |
| 79 | + |
| 80 | +The same screen has a `Tailscale` button, which installs and connects the |
| 81 | +daemon. Your machine joins your private tailnet and the gateway becomes |
| 82 | +reachable at a `100.x.y.z` address, or at a MagicDNS name like |
| 83 | +`http://your-host:20128`. |
| 84 | + |
| 85 | +**When it fits:** almost always. Only devices you enrolled in your tailnet can |
| 86 | +reach the gateway — the address is not public, and there is nothing for a |
| 87 | +stranger to find. |
| 88 | + |
| 89 | +**Setting it up by hand**, which is also how you do it for OmniRoute and LiteLLM: |
| 90 | + |
| 91 | +```bash |
| 92 | +# 1. On the host that runs the gateway |
| 93 | +curl -fsSL https://tailscale.com/install.sh | sh |
| 94 | +sudo tailscale up |
| 95 | +
|
| 96 | +# 2. Find the address it received |
| 97 | +tailscale ip -4 # e.g. 100.101.102.103 |
| 98 | +
|
| 99 | +# 3. Publish the gateway on the tailnet interface instead of loopback |
| 100 | +# (in the compose, replace 127.0.0.1 with the tailnet address) |
| 101 | +ports: |
| 102 | + - "100.101.102.103:20128:20128" |
| 103 | +
|
| 104 | +# 4. From another device already in the tailnet |
| 105 | +curl http://100.101.102.103:20128/v1/models |
| 106 | +``` |
| 107 | + |
| 108 | +Binding to the tailnet address rather than `0.0.0.0` matters: `0.0.0.0` also |
| 109 | +exposes the gateway to the local network — the café Wi-Fi, the office VLAN — |
| 110 | +which is exactly what you were avoiding. |
| 111 | + |
| 112 | +**MagicDNS** makes this readable: with it on, `http://your-host:20128` works from |
| 113 | +any device in the tailnet, and the address survives a change of IP. |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +## Option 3 — your own reverse proxy |
| 118 | + |
| 119 | +A VPS with Caddy or nginx in front, TLS terminated there, Basic Auth or mTLS on |
| 120 | +top. More work, and the only option that lets you put your own authentication |
| 121 | +layer in front of the gateway instead of relying on its flags. |
| 122 | + |
| 123 | +Worth it when several people share one gateway and you want access logs and |
| 124 | +revocation per person — neither of which the gateway's own login gives you. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +## Which one |
| 129 | + |
| 130 | +| | Tunnel | Tailscale | Reverse proxy | |
| 131 | +| :--- | :--- | :--- | :--- | |
| 132 | +| Who can reach it | anyone with the URL | only your tailnet | whoever you let through | |
| 133 | +| Setup | one button | one button, or 3 commands | real work | |
| 134 | +| Stable address | no (unless named) | yes (MagicDNS) | yes | |
| 135 | +| Works for OmniRoute / LiteLLM | no built-in button | **yes**, same recipe | yes | |
| 136 | +| Sensible default | for a one-off demo | **for everyday use** | shared or audited setups | |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## After exposing it, check what you exposed |
| 141 | + |
| 142 | +```bash |
| 143 | +# From another device, WITHOUT credentials — both should refuse |
| 144 | +curl -si https://<your-address>/v1/models | head -1 # expect 401 |
| 145 | +curl -si https://<your-address>/ | head -1 # expect 401 or a login page |
| 146 | +
|
| 147 | +# The synchronizer panel should not be exposed at all |
| 148 | +curl -si https://<your-address>:9091/ | head -1 # expect connection refused |
| 149 | +``` |
| 150 | + |
| 151 | +The panel of this synchronizer has no reason to leave the machine: it reads the |
| 152 | +gateway's database and shows credentials' health. Keep its port on `127.0.0.1` |
| 153 | +and reach it through the same tunnel or tailnet you use for everything else. |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +# Em português |
| 158 | + |
| 159 | +Alcançar o gateway de outra máquina tem três respostas usuais. Elas diferem em |
| 160 | +**quem consegue chegar até você**, e a ordem em que você liga as coisas decide se |
| 161 | +isso é seguro. |
| 162 | + |
| 163 | +> **Esta página é sobre entrar.** Fazer o tráfego **sair** por um endereço |
| 164 | +> escolhido, para que cada conta tenha o seu IP, é outro problema — veja |
| 165 | +> [Egress and Multi-Session](Egress-And-Multi-Session). O Tailscale aparece nas |
| 166 | +> duas páginas fazendo trabalhos diferentes. |
| 167 | + |
| 168 | +## Leia antes de ligar qualquer coisa |
| 169 | + |
| 170 | +As stacks deste repositório sobem com `REQUIRE_API_KEY=false` e |
| 171 | +`REQUIRE_LOGIN=false`. Isso é **seguro enquanto a porta está presa em |
| 172 | +`127.0.0.1`** — só a sua máquina alcança, e exigir senha de si mesmo no |
| 173 | +localhost acrescenta atrito sem acrescentar segurança. |
| 174 | + |
| 175 | +No instante em que o gateway é exposto, o raciocínio se inverte. O painel do |
| 176 | +9Router avisa, e o aviso não é decorativo: |
| 177 | + |
| 178 | +> *Enable "Require login" and set a custom password before activating the tunnel.* |
| 179 | + |
| 180 | +Dois detalhes tornam isso mais sério do que parece: |
| 181 | + |
| 182 | +1. **`/v1` é prefixo público.** Em `src/dashboardGuard.js`, os endpoints de |
| 183 | + inferência do gateway (`/v1`, `/v1beta`, `/api/v1`, `/codex`, `/responses`) |
| 184 | + são públicos por projeto — é assim que as ferramentas de código chamam o gateway |
| 185 | + sem sessão. Com `REQUIRE_API_KEY=false` e o gateway numa URL pública, |
| 186 | + **qualquer um que descubra o endereço gasta as suas contas**. |
| 187 | +2. **As rotas administrativas do gateway também caem.** As dele — `/api/settings`, |
| 188 | + `/api/keys`, `/api/providers` e afins do gateway — só são protegidas **quando `requireLogin` está |
| 189 | + ligado**. Com ele desligado, um gateway exposto entrega a própria |
| 190 | + configuração. |
| 191 | + |
| 192 | +A ordem, portanto, não é preferência: |
| 193 | + |
| 194 | +``` |
| 195 | +1. REQUIRE_LOGIN=true + uma senha escolhida por você |
| 196 | +2. REQUIRE_API_KEY=true + uma chave para as suas ferramentas |
| 197 | +3. só então, o túnel ou o Tailscale |
| 198 | +``` |
| 199 | + |
| 200 | +## Opção 1 — túnel Cloudflare (nativo do 9Router) |
| 201 | + |
| 202 | +A tela **API Endpoint** tem o botão `Tunnel`. Ele registra um quick tunnel da |
| 203 | +Cloudflare e devolve um endereço público `https://…trycloudflare.com` que |
| 204 | +alcança o gateway sem abrir porta nenhuma no seu roteador. |
| 205 | + |
| 206 | +**Quando serve:** você precisa de uma URL alcançável de qualquer lugar, |
| 207 | +inclusive de dispositivos que você não controla, e aceita que o endereço seja |
| 208 | +público para quem o tiver. |
| 209 | + |
| 210 | +**O que saber:** a URL é pública e não há lista de permissão — entre a internet |
| 211 | +e as suas contas existem apenas `REQUIRE_LOGIN` e `REQUIRE_API_KEY`. O endereço |
| 212 | +muda a cada reativação, a menos que você use um túnel nomeado seu. E o bloqueio |
| 213 | +do botão enquanto o login está desligado vive **na tela**: o `POST |
| 214 | +/api/tunnel/enable` do gateway não reavalia a condição. Ligue as duas variáveis antes e a |
| 215 | +questão não se coloca. |
| 216 | + |
| 217 | +## Opção 2 — Tailscale (nativo do 9Router, e o que preferir) |
| 218 | + |
| 219 | +A mesma tela tem o botão `Tailscale`, que instala e conecta o daemon. A sua |
| 220 | +máquina entra na sua tailnet e o gateway passa a ser alcançável num endereço |
| 221 | +`100.x.y.z`, ou num nome MagicDNS como `http://seu-host:20128`. |
| 222 | + |
| 223 | +**Quando serve:** quase sempre. Só os dispositivos que você cadastrou alcançam o |
| 224 | +gateway — o endereço não é público e não há o que um estranho descubra. |
| 225 | + |
| 226 | +**Configurando à mão**, que é também como se faz no OmniRoute e no LiteLLM: |
| 227 | + |
| 228 | +```bash |
| 229 | +# 1. No host que roda o gateway |
| 230 | +curl -fsSL https://tailscale.com/install.sh | sh |
| 231 | +sudo tailscale up |
| 232 | +
|
| 233 | +# 2. Descubra o endereço recebido |
| 234 | +tailscale ip -4 # ex.: 100.101.102.103 |
| 235 | +
|
| 236 | +# 3. Publique o gateway na interface da tailnet, em vez do loopback |
| 237 | +ports: |
| 238 | + - "100.101.102.103:20128:20128" |
| 239 | +
|
| 240 | +# 4. De outro dispositivo já na tailnet |
| 241 | +curl http://100.101.102.103:20128/v1/models |
| 242 | +``` |
| 243 | + |
| 244 | +Prender no endereço da tailnet em vez de `0.0.0.0` importa: `0.0.0.0` também |
| 245 | +expõe o gateway à rede local — o Wi-Fi do café, a VLAN do escritório — que é |
| 246 | +justamente o que se queria evitar. |
| 247 | + |
| 248 | +## Opção 3 — proxy reverso próprio |
| 249 | + |
| 250 | +Um VPS com Caddy ou nginx na frente, TLS terminado ali, Basic Auth ou mTLS por |
| 251 | +cima. Dá mais trabalho, e é a única opção que permite colocar a **sua** camada |
| 252 | +de autenticação na frente do gateway em vez de depender das flags dele. |
| 253 | + |
| 254 | +Compensa quando várias pessoas dividem um gateway e você quer log de acesso e |
| 255 | +revogação por pessoa — coisas que o login do gateway não oferece. |
| 256 | + |
| 257 | +## Depois de expor, confira o que você expôs |
| 258 | + |
| 259 | +```bash |
| 260 | +# De outro dispositivo, SEM credencial — os dois têm de recusar |
| 261 | +curl -si https://<seu-endereco>/v1/models | head -1 # espera-se 401 |
| 262 | +curl -si https://<seu-endereco>/ | head -1 # espera-se 401 ou tela de login |
| 263 | +``` |
| 264 | + |
| 265 | +O painel deste sincronizador não tem motivo para sair da máquina: ele lê o banco |
| 266 | +do gateway e mostra a saúde das credenciais. Mantenha a porta dele em |
| 267 | +`127.0.0.1` e alcance-o pelo mesmo túnel ou tailnet que você já usa. |
0 commit comments