• Press kit
  • Support
  • Privacy
  • Imprint
Cuevello

Set up Cuevello Relay

Why relay? Requirements DNS TLS certificate Docker Certificate renewal Firewall Mac Server Pairing Debugging

Why a relay?

Cuevello normally connects directly to the Mac on the local network. A relay is only needed when the client cannot reach the Mac directly, for example over cellular, from another Wi-Fi network, or behind NAT and firewall boundaries.

The Mac Server opens an outbound encrypted control connection to the relay. The client connects to the relay as well. The relay forwards traffic to the registered Mac; pairing, TLS, and certificate pinning remain the security foundation.

Requirements

  • A VPS or server with a public IPv4 address.
  • Docker and Docker Compose.
  • A relay subdomain, for example relay.example.com.
  • A wildcard DNS record for device subdomains.
  • A TLS certificate for the relay domain.
  • Open TCP ports for client and Mac Server control connections.

1. Configure DNS

The client connects to a device-specific subdomain in the form <device-id>.<relay-host>. Create both the relay record and the wildcard record.

relay.example.com      A      203.0.113.10
*.relay.example.com    A      203.0.113.10

Verify DNS:

dig +short relay.example.com A
dig +short abc123.relay.example.com A

2. Create the TLS certificate

The certificate is used for the Mac server control connection and must cover relay.example.com. The client port uses TLS passthrough to the Mac server, so a wildcard TLS certificate is not required.

certbot certonly --manual --preferred-challenges dns \
  -d relay.example.com \
  --agree-tos \
  --email you@example.com \
  --no-eff-email

Certbot shows TXT records for _acme-challenge.relay.example.com. Add all requested TXT values before continuing.

/etc/letsencrypt/live/relay.example.com/fullchain.pem
/etc/letsencrypt/live/relay.example.com/privkey.pem

3. Start the Docker container

The relay container is published from the dedicated repository github.com/hosy/cuevello-relay.

Download Compose template

mkdir -p /opt/cuevello-relay/certs
cd /opt/cuevello-relay
sudo cp /etc/letsencrypt/live/relay.example.com/fullchain.pem ./certs/fullchain.pem
sudo cp /etc/letsencrypt/live/relay.example.com/privkey.pem ./certs/privkey.pem
services:
  cuevello-relay:
    image: ghcr.io/hosy/cuevello-relay:latest
    container_name: cuevello-relay
    restart: unless-stopped
    environment:
      RELAY_SECRET_PEPPER: "replace-with-at-least-32-random-bytes"
      TLS_CERT_FILE: "/certs/fullchain.pem"
      TLS_KEY_FILE: "/certs/privkey.pem"
      MAX_STREAMS_PER_DEVICE: "32"
      CLIENT_RATE_LIMIT_PER_MINUTE: "120"
      CONTROL_RATE_LIMIT_PER_MINUTE: "30"
      REQUIRE_SIGNED_REGISTRATION: "true"
    ports:
      - "9443:443"
      - "9444:8443"
    volumes:
      - ./certs:/certs:ro
      - relay-data:/data

volumes:
  relay-data:

Generate a long secret pepper:

openssl rand -base64 48

After saving the Compose file, let the unprivileged cuevello user in the container read the private key, then start the relay:

RELAY_UID="$(docker compose run --rm --no-deps --entrypoint id cuevello-relay -u)"
sudo chown "$RELAY_UID" ./certs/privkey.pem
sudo chmod 600 ./certs/privkey.pem
sudo chmod 644 ./certs/fullchain.pem
docker compose run --rm --no-deps --entrypoint test cuevello-relay -r /certs/privkey.pem
docker compose up -d
docker compose ps
docker compose logs --tail=100

Renew the TLS certificate

Run these commands on the relay host. Check the certificate name, domains, and expiry date first:

sudo certbot certificates

If it has expired, repeat the DNS challenge. Include every domain listed for that certificate; add -d '*.relay.example.com' if it also covers the wildcard name. Add all TXT values requested by Certbot before continuing.

sudo certbot certonly --manual --preferred-challenges dns \
  --cert-name relay.example.com \
  -d relay.example.com

If Certbot already has a valid certificate, skip issuance. Copy the valid certificate into the mounted directory, allow the container user to read the key, and restart the relay. Use the path reported by Certbot if it differs from this example.

cd /opt/cuevello-relay
sudo cp /etc/letsencrypt/live/relay.example.com/fullchain.pem ./certs/fullchain.pem
sudo cp /etc/letsencrypt/live/relay.example.com/privkey.pem ./certs/privkey.pem
RELAY_UID="$(docker compose run --rm --no-deps --entrypoint id cuevello-relay -u)"
sudo chown "$RELAY_UID" ./certs/privkey.pem
sudo chmod 600 ./certs/privkey.pem
sudo chmod 644 ./certs/fullchain.pem
docker compose run --rm --no-deps --entrypoint test cuevello-relay -r /certs/privkey.pem
docker compose restart cuevello-relay
docker compose logs --tail=50 cuevello-relay
echo | openssl s_client -connect relay.example.com:9444 -servername relay.example.com 2>/dev/null | openssl x509 -noout -dates

Confirm that the certificate served on control port 9444 has a new notAfter date and that the Mac server reconnects. The relay loads its certificate at startup.

The manual DNS challenge does not renew unattended. For automatic renewal, use a Certbot DNS plugin for your DNS provider or manual authentication hooks, test with sudo certbot renew --dry-run, and add a deploy hook that copies the new certificate, keeps the private key readable by the container user, and restarts the relay. See the Certbot renewal guide.

4. Open the firewall

  • 9443 for clients.
  • 9444 for Mac Server control connections.
nc -vz relay.example.com 9443
nc -vz relay.example.com 9444

5. Configure the Mac Server

  1. Start Cuevello Server on the Mac.
  2. Open Relay Settings....
  3. Enable Relay.
  4. Set Host to relay.example.com.
  5. Set Client Port to 9443.
  6. Set Control Port to 9444.
  7. Apply the settings.

6. Pair the client

Open the Mac pairing window. When relay is enabled and connected, the QR code contains the relay information. The client can pair through the relay even when it is outside the local network.

Security

  • Knowing a relay URL is not enough to control a Mac. Cuevello still requires end-to-end TLS with a paired client certificate.
  • The public client port uses TLS passthrough. The relay only reads the SNI hostname needed for routing and cannot decrypt Cuevello requests.
  • Mac server control registrations are signed with a persistent device key. The relay secret alone is not sufficient to take over a registered device.
  • The container enforces TLS 1.3, bounded registration requests, per-IP rate limits, and per-device stream limits.
  • Keep RELAY_SECRET_PEPPER, /data/devices.json, and the TLS private key private.

Debugging

DEVICE_ID="replace-with-device-id"
HOST="$DEVICE_ID.relay.example.com"

dig +short "$HOST" A
nc -vz "$HOST" 9443
openssl s_client -connect "$HOST:9443" -servername "$HOST"
cd /opt/cuevello-relay
docker compose logs -f --tail=100

Warum ein Relay?

Cuevello verbindet sich normalerweise direkt mit dem Mac im lokalen Netzwerk. Ein Relay wird nur benötigt, wenn der Client den Mac nicht direkt erreichen kann, zum Beispiel über Mobilfunk, aus einem anderen WLAN oder hinter NAT- und Firewall-Grenzen.

Der Mac Server baut selbst eine ausgehende, verschlüsselte Control-Verbindung zum Relay auf. Der Client verbindet sich ebenfalls mit dem Relay. Das Relay leitet den Verkehr zum registrierten Mac weiter; Pairing, TLS und Zertifikat-Pinning bleiben die Sicherheitsbasis.

Voraussetzungen

  • Ein VPS oder Server mit öffentlicher IPv4-Adresse.
  • Docker und Docker Compose.
  • Eine Relay-Subdomain, zum Beispiel relay.example.com.
  • Ein Wildcard-DNS-Eintrag für Device-Subdomains.
  • Ein TLS-Zertifikat für die Relay-Domain.
  • Offene TCP-Ports für Client- und Mac-Server-Control-Verbindungen.

1. DNS einrichten

Der Client verbindet sich mit einer Device-Subdomain im Format <device-id>.<relay-host>. Lege deshalb den Relay-Eintrag und den Wildcard-Eintrag an.

relay.example.com      A      203.0.113.10
*.relay.example.com    A      203.0.113.10

DNS prüfen:

dig +short relay.example.com A
dig +short abc123.relay.example.com A

2. TLS-Zertifikat erstellen

Das Zertifikat wird für die Mac-Server-Control-Verbindung genutzt und muss relay.example.com abdecken. Der Client-Port nutzt TLS-Passthrough zum Mac Server, deshalb ist kein Wildcard-TLS-Zertifikat für das Relay nötig.

certbot certonly --manual --preferred-challenges dns \
  -d relay.example.com \
  --agree-tos \
  --email you@example.com \
  --no-eff-email

Certbot zeigt TXT-Records für _acme-challenge.relay.example.com. Trage alle angeforderten TXT-Werte ein, bevor du fortfährst.

/etc/letsencrypt/live/relay.example.com/fullchain.pem
/etc/letsencrypt/live/relay.example.com/privkey.pem

3. Docker Container starten

Der Relay-Container wird aus dem eigenen Repository github.com/hosy/cuevello-relay veröffentlicht.

Compose-Vorlage herunterladen

mkdir -p /opt/cuevello-relay/certs
cd /opt/cuevello-relay
sudo cp /etc/letsencrypt/live/relay.example.com/fullchain.pem ./certs/fullchain.pem
sudo cp /etc/letsencrypt/live/relay.example.com/privkey.pem ./certs/privkey.pem
services:
  cuevello-relay:
    image: ghcr.io/hosy/cuevello-relay:latest
    container_name: cuevello-relay
    restart: unless-stopped
    environment:
      RELAY_SECRET_PEPPER: "replace-with-at-least-32-random-bytes"
      TLS_CERT_FILE: "/certs/fullchain.pem"
      TLS_KEY_FILE: "/certs/privkey.pem"
      MAX_STREAMS_PER_DEVICE: "32"
      CLIENT_RATE_LIMIT_PER_MINUTE: "120"
      CONTROL_RATE_LIMIT_PER_MINUTE: "30"
      REQUIRE_SIGNED_REGISTRATION: "true"
    ports:
      - "9443:443"
      - "9444:8443"
    volumes:
      - ./certs:/certs:ro
      - relay-data:/data

volumes:
  relay-data:

Erzeuge eine lange zufällige Secret-Pepper-Zeichenkette:

openssl rand -base64 48

Nachdem du die Compose-Datei gespeichert hast, gib dem nicht privilegierten Benutzer cuevello im Container Lesezugriff auf den privaten Schlüssel und starte das Relay:

RELAY_UID="$(docker compose run --rm --no-deps --entrypoint id cuevello-relay -u)"
sudo chown "$RELAY_UID" ./certs/privkey.pem
sudo chmod 600 ./certs/privkey.pem
sudo chmod 644 ./certs/fullchain.pem
docker compose run --rm --no-deps --entrypoint test cuevello-relay -r /certs/privkey.pem
docker compose up -d
docker compose ps
docker compose logs --tail=100

TLS-Zertifikat erneuern

Führe diese Befehle auf dem Relay-Server aus. Prüfe zuerst den Zertifikatsnamen, die Domains und das Ablaufdatum:

sudo certbot certificates

Falls das Zertifikat abgelaufen ist, wiederhole die DNS-Challenge. Gib alle für dieses Zertifikat aufgelisteten Domains an; ergänze -d '*.relay.example.com', falls es auch den Wildcard-Namen enthält. Trage alle von Certbot verlangten TXT-Werte ein, bevor du fortfährst.

sudo certbot certonly --manual --preferred-challenges dns \
  --cert-name relay.example.com \
  -d relay.example.com

Wenn bei Certbot bereits ein gültiges Zertifikat vorliegt, überspringe die Ausstellung. Kopiere das gültige Zertifikat in das eingebundene Verzeichnis, gib dem Container-Benutzer Lesezugriff auf den Schlüssel und starte das Relay neu. Verwende den von Certbot angezeigten Pfad, falls er vom Beispiel abweicht.

cd /opt/cuevello-relay
sudo cp /etc/letsencrypt/live/relay.example.com/fullchain.pem ./certs/fullchain.pem
sudo cp /etc/letsencrypt/live/relay.example.com/privkey.pem ./certs/privkey.pem
RELAY_UID="$(docker compose run --rm --no-deps --entrypoint id cuevello-relay -u)"
sudo chown "$RELAY_UID" ./certs/privkey.pem
sudo chmod 600 ./certs/privkey.pem
sudo chmod 644 ./certs/fullchain.pem
docker compose run --rm --no-deps --entrypoint test cuevello-relay -r /certs/privkey.pem
docker compose restart cuevello-relay
docker compose logs --tail=50 cuevello-relay
echo | openssl s_client -connect relay.example.com:9444 -servername relay.example.com 2>/dev/null | openssl x509 -noout -dates

Prüfe, ob das über Control-Port 9444 ausgelieferte Zertifikat ein neues notAfter-Datum hat und der Mac Server wieder verbunden ist. Das Relay lädt sein Zertifikat beim Start.

Die manuelle DNS-Challenge erneuert sich nicht unbeaufsichtigt. Für die automatische Erneuerung verwende ein Certbot-DNS-Plugin deines DNS-Anbieters oder manuelle Authentifizierungs-Hooks, teste mit sudo certbot renew --dry-run und richte einen Deploy-Hook ein. Dieser muss das neue Zertifikat kopieren, den Schlüssel für den Container-Benutzer lesbar halten und das Relay neu starten. Siehe die Certbot-Anleitung zur Erneuerung.

4. Firewall freigeben

  • 9443 für Clients.
  • 9444 für Mac-Server-Control-Verbindungen.
nc -vz relay.example.com 9443
nc -vz relay.example.com 9444

5. Mac Server konfigurieren

  1. Cuevello Server auf dem Mac starten.
  2. Relay Settings... öffnen.
  3. Relay aktivieren.
  4. Host auf relay.example.com setzen.
  5. Client Port auf 9443 setzen.
  6. Control Port auf 9444 setzen.
  7. Einstellungen anwenden.

6. Client pairen

Öffne das Pairing-Fenster am Mac. Wenn Relay aktiv und verbunden ist, enthält der QR-Code die Relay-Daten. Der Client kann dann auch außerhalb des lokalen Netzwerks über das Relay pairen.

Sicherheit

  • Die Relay-URL allein reicht nicht aus, um einen Mac zu steuern. Cuevello verlangt weiterhin Ende-zu-Ende-TLS mit einem gepairten Client-Zertifikat.
  • Der öffentliche Client-Port nutzt TLS-Passthrough. Das Relay liest nur den SNI-Hostnamen für das Routing und kann Cuevello-Requests nicht entschlüsseln.
  • Mac-Server-Control-Registrierungen werden mit einem persistenten Device-Key signiert. Das Relay-Secret allein reicht nicht aus, um ein registriertes Device zu übernehmen.
  • Der Container erzwingt TLS 1.3, begrenzte Registrierungs-Requests, IP-basierte Rate-Limits und Limits pro Device-Stream.
  • Halte RELAY_SECRET_PEPPER, /data/devices.json und den privaten TLS-Schlüssel geheim.

Debugging

DEVICE_ID="replace-with-device-id"
HOST="$DEVICE_ID.relay.example.com"

dig +short "$HOST" A
nc -vz "$HOST" 9443
openssl s_client -connect "$HOST:9443" -servername "$HOST"
cd /opt/cuevello-relay
docker compose logs -f --tail=100
Cuevello
Press kit Support Privacy Imprint