,

Web-terminal i nettleseren med FastAPI og Cloudflare Tunnel

Web-terminal i nettleseren med FastAPI og Cloudflare Tunnel

Du står et sted med elendig nett og trenger å kjøre én kommando på en server hjemme eller i produksjon. Å pirke på en SSH-klient på telefonen er en øvelse i tålmodighet. Løsningen jeg endte på: en terminal rett i nettleseren, tilgjengelig hvor som helst fra, uten en eneste åpen port. Under følger hvordan, og hvorfor autentiseringen må komme først av alt.

Arkitektur

Arkitekturdiagram som viser nettleser med xterm.js koblet via WebSocket gjennom Cloudflare Tunnel til FastAPI-backend og pty på serveren

Det er fire deler i dette. Backenden i FastAPI eksponerer en WebSocket som kobler seg mot en pty (pseudo-terminal) på serveren. Frontenden bruker xterm.js til å tegne selve terminalen og sende tastetrykkene tilbake over den samme WebSocket-en. Hele greia kjøres som en systemd-service. Ekstern tilgang går via Cloudflare Tunnel, og dermed har maskinen ingen innkommende porter åpne i det hele tatt.

Backend: FastAPI mot pty

En pty er det som gir deg et ekte terminal-grensesnitt, ikke bare en rå strøm av bytes. Serveren spawner et skall, kobler master-enden til WebSocket-en, og pumper så bytes i begge retninger.

import asyncio, os, pty
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def terminal(ws: WebSocket):
    await ws.accept()
    pid, fd = pty.fork()
    if pid == 0:
        os.execvp("bash", ["bash"])
    loop = asyncio.get_event_loop()

    async def to_browser():
        while True:
            data = await loop.run_in_executor(None, os.read, fd, 1024)
            await ws.send_bytes(data)

    task = asyncio.create_task(to_browser())
    try:
        while True:
            msg = await ws.receive_bytes()
            os.write(fd, msg)
    finally:
        task.cancel()
        os.close(fd)

Frontend: xterm.js

xterm.js tar seg av rendering, escape-sekvenser og input, altså alt det kjedelige du ikke har lyst til å skrive selv. Det som gjenstår er å koble mot WebSocket-en og sende tastetrykkene videre som bytes.

const term = new Terminal();
term.open(document.getElementById("terminal"));
const ws = new WebSocket("wss://terminal.eksempel.no/ws");
ws.binaryType = "arraybuffer";
ws.onmessage = (e) => term.write(new Uint8Array(e.data));
term.onData((d) => ws.send(new TextEncoder().encode(d)));

Systemd og Cloudflare Tunnel

Nettverkstopologi som viser systemd-tjeneste bundet til localhost og Cloudflare Tunnel som gir ekstern tilgang uten åpne brannmurporter

Kjør tjenesten som en service, så den kommer opp igjen av seg selv etter en omstart.

[Unit]
Description=Web terminal
After=network.target

[Service]
ExecStart=/opt/webterm/venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
Restart=always
User=webterm

[Install]
WantedBy=multi-user.target

Legg merke til bindingen til 127.0.0.1. Cloudflare Tunnel kobler utover mot Cloudflare, og det betyr at ingen porter trenger å åpnes i brannmuren.

tunnel: web-terminal
ingress:
  - hostname: terminal.eksempel.no
    service: http://127.0.0.1:8000
  - service: http_status:404

Sikkerhet er ikke valgfritt

En åpen web-terminal er nøyaktig det det høres ut som: full tilgang til maskinen for hvem som helst som finner den. Derfor må autentiseringen stå foran WebSocket-en, ikke bak. Legg en Cloudflare Access-policy foran hostnavnet, med tilgang begrenset til navngitte e-postadresser eller en identitetsleverandør. Alternativet er et token som valideres før WebSocket-en overhodet aksepteres. Kravene, kort:

  • Access-policy eller token foran alt, og avvis før pty-en spawnes.
  • Begrens hvem som slipper inn til en eksplisitt liste, ikke «alle som har lenken».
  • Logg hver eneste tilkobling: kilde, identitet og tidspunkt.
  • Kjør tjenesten som en dedikert bruker med minst mulig rettigheter.

Oppsummert

FastAPI og en pty gir deg selve terminalen, xterm.js gir grensesnittet, systemd holder den i live, og Cloudflare Tunnel gir tilgang uten åpne porter. Men glemmer du autentiseringen foran WebSocket-en, har du i praksis gitt bort maskinen. Sett opp Access-policyen først, så tar du resten etterpå.


Trenger du hjelp med dette?

Ta kontakt for en uforpliktende prat om hvordan jeg kan hjelpe deg.

Navn