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
- Start Cuevello Server on the Mac.
- Open Relay Settings....
- Enable Relay.
- Set Host to
relay.example.com.
- Set Client Port to
9443.
- Set Control Port to
9444.
- 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
- Cuevello Server auf dem Mac starten.
- Relay Settings... öffnen.
- Relay aktivieren.
- Host auf
relay.example.com setzen.
- Client Port auf
9443 setzen.
- Control Port auf
9444 setzen.
- 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