Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Transport sécurisé : configuration de bout en bout

Cette page constitue la référence complète de configuration pour connecter un client à un démon de manière sécurisée, selon trois topologies :

  1. Client direct vers le démon - WSS avec TLS mutuel, sans relais.
  2. Démon vers le relais - le démon maintient un pont sortant vers un relais désigné afin qu’il soit joignable derrière un NAT/CGNAT.
  3. Client vers le relais - le client atteint le daemon via ce relais, tandis que le véritable mTLS client<->daemon se termine toujours au niveau du daemon.

Pour le démarrage rapide en 60 secondes, consultez Configuration à distance (WSS) ; cette page est le guide détaillé, option par option.

Modèle mental : deux enveloppes, une frontière de confiance

Il existe deux couches TLS, et une seule d’entre elles constitue la frontière de sécurité :

  • mTLS interne (la véritable frontière). TLS 1.3 uniquement, avec authentification mutuelle. Le client présente un certificat émis par le daemon ; le daemon présente son certificat feuille de serveur. Ce plan est le plan RPC. Il n’existe aucun chemin réservé au serveur ou non authentifié : un certificat client est toujours requis. Le certificat admet l’appareil ; il ne constitue pas un identifiant RPC. Chaque initialize distant doit également transporter un jeton porteur auth_token qui identifie le principal, faute de quoi le daemon le refuse avec AUTH_REQUIRED (-32010). Voir Jeton porteur.
  • TLS externe (une frontière de métadonnées). Lorsqu’un relais se trouve sur le chemin, il termine une session TLS + WebSocket externe et transmet un texte chiffré opaque. Il ne détient jamais de clé lui permettant de lire le RPC interne. Dans la topologie directe, il n’y a pas de couche externe.

Ports par défaut (tous configurables) :

PlanPar défautConfig
Démon WSS (RPC mTLS interne)9781[wss].port
Point de terminaison d’inscription du daemon9782[enroll].port
Relais (TLS externe + WS)8443relayer --bind / [bind]

Partout, <data_dir> désigne le répertoire de données du démon (généralement ~/.zeroclaw) et <config-dir> désigne le répertoire de configuration zerocode du client (--config-dir, généralement ~/.zeroclaw). Les fichiers de configuration ne développent pas ~ ; utilisez des chemins absolus.


Topologie 1 : client connecté directement au démon

zerocode  ===== mutual-TLS WSS (TLS 1.3) =====>  daemon  [wss] :9781

1a. Côté démon

Activez l’écouteur WSS. Le paramètre par défaut sécurisé consiste à laisser le démon générer automatiquement sa propre autorité de certification et son certificat serveur lors du premier démarrage, afin que vous n’ayez pas à gérer manuellement les éléments TLS :

[wss]
enabled = true
# bind = "0.0.0.0"   # par défaut
# port = 9781        # par défaut
# Laissez cert_path/key_path vides pour générer automatiquement un certificat serveur dans
# <data_dir>/tls/ au premier démarrage. Définissez-les uniquement pour utiliser votre propre certificat serveur.

Lors du premier démarrage avec [wss].enabled = true, le démon écrit, sous <data_dir>/tls/ (répertoire avec le mode 0700) :

FichierObjectifMode
ca.crtCertificat d’autorité de certification (public) par démonumask par défaut
ca.keyClé privée de l’autorité de certification (signe les certificats client)0600
server.crtCertificat feuille du serveur WSS (SAN localhost, 127.0.0.1)umask par défaut
server.keyclé privée du serveur WSS0600

Les clés privées sont écrites avec les permissions 0600 ; les certificats publics utilisent le masque umask du processus. Le répertoire tls/ lui-même est en 0700.

L’autorité de certification (CA) n’est jamais renouvelée silencieusement : si ca.crt et ca.key existent, ils sont réutilisés. La durée de vie de la CA générée automatiquement est de 10 ans ; le certificat feuille du serveur est d’environ ~27 mois ; les certificats client émis ont une durée de 30 jours.

Ouvrez le port (sudo ufw allow 9781/tcp) et démarrez le démon. Vous devriez voir une ligne de journal indiquant que l’écouteur WSS est actif sur 0.0.0.0:9781.

Le client a désormais besoin d’un certificat. Il existe deux façons d’en obtenir un.

1b. Côté client - option A : enrôlement (recommandé)

L’enrôlement fournit à un client sans certificat son premier certificat via un point de terminaison authentifié par le serveur et protégé par appairage, sans gestion manuelle des certificats. Activez-le :

[enroll]
enabled = true
# bind = "0.0.0.0"   # default
# port = 9782        # default
# Requires [wss] enabled and a daemon CA key (auto-generated above, or BYO+key).
# If the CA key is absent the endpoint fails closed and certs must be provisioned
# out of band.

Au démarrage, le démon affiche dans sa console/ses journaux un code d’association et une chaîne d’authentification courte (SAS). Le code est à usage unique et expire 10 minutes après sa génération - c’est le seul justificatif d’authentification au porteur pour l’émission de certificats, et il se retrouve dans les consoles et les journaux ; un code copié doit donc cesser de fonctionner peu après que l’opérateur l’a utilisé. Un code expiré est refusé et supprimé ; générez-en un nouveau depuis l’API d’association de la passerelle lorsque vous en avez besoin. Ensuite, sur le poste de travail :

# Interactif : un client sans certificat s’inscrit automatiquement lors de la première connexion.
zerocode --connect wss://<remote-host>:9781

# Ou explicitement / en mode non interactif :
zerocode --enroll --connect wss://<remote-host>:9781

zerocode demande le code d’association, génère localement une clé P-256 et un CSR localement (la clé privée ne quitte jamais l’appareil), et affiche le SAS. Vérifiez que le SAS correspond à celui affiché par le démon (cela permet de détecter une autorité de certification homme-du-milieu), et il met en cache, sous <config-dir>/tls/:

FichierObjectifMode
client.crtCertificat client émisumask par défaut
client.keyClé privée du client (générée localement)0600
ca.crtChaîne d’autorités de certification du démon, épinglée pour le plan RPCumask par défaut
profile.jsondevice_id, not_after, profil de relais mis en cacheumask par défaut

Les exécutions ultérieures réutilisent le certificat en cache sans procéder à une nouvelle inscription. Le certificat est renouvelé automatiquement à environ 50 % de sa durée de vie (environ 15 jours) via la session mTLS active ; un certificat révoqué ne peut pas se renouveler lui-même.

L’inscription ne délivre qu’un certificat, et non un jeton Bearer. Avant que le daemon n’accepte une session, appairez-vous avec la passerelle pour obtenir un jeton zc_... et transmettez-le à zerocode via ZEROCLAW_AUTH_TOKEN, auth_token_file ou auth_token. Une fois le jeton en place, zerocode --connect wss://<remote-host>:9781 (ou simplement zerocode si uri est défini dans la configuration) ne nécessite plus aucun autre drapeau.

Valeurs par défaut du point de terminaison d’inscription : --enroll-host utilise par défaut l’hôte de --connect ; --enroll-port utilise par défaut 9782.

La première version exige intentionnellement un code d’appairage pour chaque enrôlement. Le paramètre réservé allow_unpaired_enrollment est rejeté au démarrage du démon jusqu’à ce que le client dispose d’une ancre de confiance explicite sans code, telle qu’une empreinte d’AC du démon épinglée. Cela empêche le chemin TLS d’enrôlement provisoire de se transformer en confiance aveugle à la première utilisation.

1c. Côté client - option B : certificat émis par l’opérateur

Si vous préférez générer un certificat sur le démon et le copier ailleurs :

# Sur l’hôte du démon. --out-dir écrit également un fichier ca.crt/client.crt/client.key directement utilisable.
zeroclaw security issue-client-cert --name my-laptop --out-dir /tmp/my-laptop-tls
# ajouter --force pour écraser un certificat existant pour ce nom

Copiez les trois fichiers dans le <config-dir>/tls/ du client sous les noms ca.crt, client.crt et client.key (alors zerocode --connect wss://host:9781 les trouve automatiquement), ou indiquez-les explicitement :

zerocode --connect wss://<remote-host>:9781 \
  --tls-ca-cert  /path/ca.crt \
  --tls-client-cert /path/client.crt \
  --tls-client-key  /path/client.key

Configuration équivalente (pour que zerocode fonctionne tel quel) :

[connection.wss]
uri = "wss://<remote-host>:9781"

[connection.wss.tls]
ca_cert_path     = "/abs/path/ca.crt"
client_cert_path = "/abs/path/client.crt"
client_key_path  = "/abs/path/client.key"

zerocode seul nécessite toujours un jeton d’authentification Bearer, fourni par ZEROCLAW_AUTH_TOKEN ou par auth_token_file / auth_token sous [connection.wss].

Un client sans certificat qui atteint le plan WSS sans s’être enrôlé reçoit un message exploitable « inscrivez-vous d’abord » (et le démon consigne le client non migré rejeté) — jamais de blocage silencieux. --tls-skip-verify n’assouplit que la vérification du serveur pour un démon de développement autosigné ; le certificat client reste requis.


Topologie 2 : du démon au relais

daemon  ====== outbound: register + bridge ======>  relay  :8443
[relay]                                             (blind forwarder)

Le démon se connecte au relais, prouve une identité Ed25519 stable et enregistre un node-id. Les clients se connectent ensuite à ce node-id (topologie 3). Le relais ne relaie que du texte chiffré.

2a. Lancez le relais (zerorelay)

Configurez avec relay.toml (voir apps/zerorelay/relay.example.toml) ; chaque option de ligne de commande remplace la valeur correspondante du fichier. La section [admission] est rechargée à chaud lors de SIGHUP ; tout rechargement qui transformerait un relais public en admission ouverte sans jeton sans activation explicite est refusé, et la politique précédente reste active.

# relay.toml
bind = "0.0.0.0:8443"

[tls]
# Omettez cert/key pour générer automatiquement un certificat TLS externe dans dir au premier lancement (sans
# openssl). Définissez sans avec le ou les noms d'hôte/IP publics du relais.
dir  = "/data/tls"
sans = ["relay.example.com"]
# Ou fournissez le vôtre (par ex. un certificat d'une AC publique) :
# cert = "/etc/zerorelay/fullchain.pem"
# key  = "/etc/zerorelay/privkey.pem"

[admission]
# "open" accepte tout daemon signé (sous réserve de la liste deny) ; "allowlist" n'accepte que
# les empreintes de clés publiques de daemons listées. Deny prime toujours.
mode = "open"
allow = []
deny  = []
# Un relais public (non loopback) DOIT contrôler l'enregistrement : définissez ici un secret partagé
# (chaque daemon le présente via [relay] token) ou utilisez mode = "allowlist".
# Un relais qui dessert des daemons enregistrés avec `zeroclaw relay claim` doit utiliser
# mode = "allowlist" et laisser relay_token non défini (voir 2d).
# Sinon, un relais OPEN sans jeton, sur une liaison publique, refuse de démarrer, car
# n'importe quel daemon sur Internet pourrait s'enregistrer et accaparer des node-ids non réclamés. (Une
# liaison loopback pour le développement local est exemptée ; un relais public ouvert délibéré
# peut passer outre avec allow_public_open = true.)
relay_token = "change-me-to-a-long-random-secret"

[limits]
max_conns_per_node     = 256
idle_timeout_secs      = 300
lease_ttl_secs         = 300
accept_burst_per_ip    = 30
accept_rate_per_ip     = 10.0
connect_burst_per_node = 60
connect_rate_per_node  = 20.0

Exécutez-le :

zerorelay --config /etc/zerorelay/relay.toml
# de manière équivalente, options uniquement (une liaison publique nécessite un jeton ou une liste d’autorisation, sinon le
# relay refuses to start):
zerorelay --bind 0.0.0.0:8443 --tls-san relay.example.com \
  --relay-token change-me-to-a-long-random-secret

Lorsque --tls-cert/--tls-key sont omis, le relais provisionne automatiquement une CA et un certificat serveur dans le répertoire TLS (ordre de résolution : $ZERORELAY_DATA_DIR/tls, sinon $HOME/.zerorelay/tls, sinon ./zerorelay/tls) ; localhost et 127.0.0.1 figurent toujours dans les SAN. La ca.crt provisionnée automatiquement est celle à laquelle un daemon/client fait confiance pour le TLS externe du relais.

Contrôle d’accès. Le mode open, associé à un relay_token facultatif, constitue le contrôle d’accès le plus simple. Le mode allowlist s’appuie sur l’empreinte de la clé publique d’enregistrement du démon (représentation hexadécimale SHA-256 de la clé Ed25519 située dans <data_dir>/relay/registration.key du démon) ; ajoutez les empreintes à allow (et rechargez avec kill -HUP <pid>). Un identifiant de nœud est lié à la clé publique de son premier enregistrant ; une autre clé ne peut donc pas usurper un identifiant de nœud actif (elle obtient node_taken).

Le relais vérifie relay_token avant la liste d’autorisation, dans tous les modes. Un relais qui sert des revendications en libre-service (2d) doit donc utiliser allowlist avec relay_token non défini : une revendication ne fournit jamais le jeton partagé, si bien qu’un daemon revendiqué serait refusé avec forbidden: bad relay token. La liste d’autorisation protège déjà un relais public à elle seule, donc le jeton n’y apporte rien.

Docker. apps/zerorelay/Dockerfile utilise une image distroless avec CMD ["--config", "/etc/zerorelay/relay.toml"] et un HEALTHCHECK sans shell zerorelay healthcheck --addr ; compose.yaml monte un volume sur /data afin que le TLS auto-provisionné persiste. Exposez 8443.

2b. Pointez le démon vers le relais

[wss]
enabled = true          # OBLIGATOIRE : le relais transmet vers l'écouteur WSS local

[relay]
enabled = true
url = "relay.example.com:8443"
# node_id : laissez-le VIDE (recommandé) pour générer automatiquement et conserver une capacité aléatoire de 128 bits
# dans <data_dir>/relay/node_id. Ne le définissez que pour épingler un identifiant spécifique.
# token = "change-me"   # doit correspondre à [admission].relay_token du relais, s'il est défini

# Faites confiance au certificat EXTERNE du relais — choisissez-en UN :
relay_ca_path = "/path/to/relay/ca.crt"   # faites confiance à l'autorité de certification (autosignée) du relais
# tofu = true                              # OU épinglez le certificat feuille du relais lors de la première utilisation
# relay_insecure = true                    # OU ignorez la vérification externe (développement uniquement)
# (laissez les trois options non définies pour utiliser les autorités racines publiques intégrées, pour un relais utilisant une autorité de certification publique)

[relay] nécessite l’activation de [wss] (le relais établit un pont vers 127.0.0.1:<wss.port>), et échoue en mode sécurisé si url est vide. Au démarrage, le daemon consigne l’identifiant du nœud avec l’indication “donnez ceci aux clients comme –relay-node” ; vous pouvez également le lire dans <data_dir>/relay/node_id. La clé d’enregistrement stable du daemon est créée dans <data_dir>/relay/registration.key (0600).

Priorité de confiance du certificat externe (de la plus élevée à la plus faible) : relay_insecure > relay_ca_path > un pin stocké dans <data_dir>/relay/relay_pin (explicite ou TOFU) > tofu > les racines publiques. La configuration d’une CA remplace donc un pin obsolète sans le supprimer. Avec tofu = true, l’empreinte observée du certificat feuille du relais est épinglée dans <data_dir>/relay/relay_pin, et l’enrôlement transmet ce même pin aux clients afin qu’ils épinglent le même certificat feuille.

2c. (facultatif) rotation du node-id et mTLS externe

[relay]
node_id_rotation_days = 30   # auto-rotate the auto-minted id every N days (0 = never)

La rotation génère un nouvel identifiant, l’exécute en parallèle de l’ancien pendant une période de grâce de 10 minutes afin de ne pas interrompre les clients en cours, puis retire l’ancien identifiant ; le nouvel identifiant est transmis aux clients via le canal existant lors du prochain renouvellement de leur certificat. Forcez-en une maintenant avec zeroclaw security relay-rotate-node-id (mode d’émission automatique uniquement ; un node_id épinglé ne fait jamais l’objet d’une rotation).

Pour un relais qui authentifie également les démons au niveau de la couche externe, définissez sur le relais [admission].outer_client_auth = "required" + outer_client_ca, et sur le démon [relay].outer_client_cert / outer_client_key. Cela s’ajoute au TLS externe et ne touche jamais au mTLS interne.

2d. Inscription en libre-service avec zeroclaw relay claim

Lorsque le relais est exécuté avec un plan de contrôle ZeroRelay, un opérateur peut enrôler un démon à l’aide d’un jeton de réclamation à usage unique, au lieu de modifier manuellement la liste d’autorisation du relais. Le plan de contrôle émet le jeton et affiche la commande à exécuter sur l’hôte du démon :

zeroclaw relay claim <TOKEN> --control https://control.example.com

Le daemon signe la revendication avec sa clé d’enregistrement existante, de sorte que l’empreinte que le plan de contrôle place sur la liste d’autorisation est exactement la clé avec laquelle le daemon s’enregistre. En cas de succès vérifié, le plan de contrôle ajoute cette empreinte à la liste d’autorisation du relais et recharge le relais, et le daemon écrit [relay] enabled, url, node_id et relay_host. Démarrez ou redémarrez le daemon pour vous enregistrer.

Avant de revendiquer :

  • Le relais doit admettre uniquement par empreinte. Utilisez mode = "allowlist" et laissez relay_token non défini (voir 2a). Une revendication ne transmet pas le jeton partagé.
  • Activez [wss]. Le relais refuse l’enregistrement tant que l’écouteur WSS n’est pas activé. Une revendication effectuée sans cela est tout de même enregistrée et prend effet au prochain démarrage, une fois [wss] activé.
  • Configurez d’abord la confiance pour le certificat externe du relais, comme à l’étape 2b (relay_ca_path, tofu, ou les racines publiques). La revendication n’écrit que les quatre champs ci-dessus et laisse le reste de [relay] inchangé.
  • Ne définissez pas les champs gérés par le claim via des surcharges d’environnement. Si relay.enabled, relay.url, relay.node_id ou relay.relay_host est surchargé, le claim refuse avant d’envoyer quoi que ce soit, car la surcharge supprimerait les valeurs écrites.

--control doit être https, ou http sur une adresse de bouclage. Les redirections et les proxys sont désactivés afin que le jeton ne puisse pas atteindre une autre origine. Chaque refus indique no config was written. Un jeton est à usage unique : une fois accepté par le plan de contrôle, il est consommé, même si le daemon rejette ensuite la réponse (par exemple à cause d’une adresse de relais inutilisable). Dans ce cas, corrigez la cause puis générez un nouveau jeton.


Topologie 3 : client vers relais vers démon

zerocode  ==outer TLS+WS==>  relay  ==forwards ciphertext==>  daemon
   \________________ inner mutual-TLS (TLS 1.3) terminates here _______________/

Cela combine les topologies 1 et 2 : le client a besoin d’un certificat client interne (enrôlement, comme dans 1b) et des coordonnées du relais (adresse, node-id et confiance dans le certificat externe du relais).

3a. La voie simple : l’enrôlement inclut le profil de relais

Lorsque le démon est configuré avec [relay], sa réponse d’inscription inclut un profil de relais (relay_url, node_id et l’empreinte relay_cert_pin du certificat feuille du relais). Ainsi, une seule inscription provisionne tout :

zerocode --enroll --connect wss://<daemon-host>:9781

zerocode met en cache le certificat interne et le profil du relais dans <config-dir>/tls/profile.json. Ensuite, zerocode simple atteint le démon via le relais sans options - il connaît déjà l’adresse du relais, le node-id et le PIN.

3b. Le chemin manuel

Donnez explicitement au client les coordonnées du relais. Le certificat interne provient toujours de l’enrôlement ou de --tls-* (topologie 1) :

zerocode \
  --relay      relay.example.com:8443 \
  --relay-node <node-id-from-daemon-log> \
  --relay-ca   /path/to/relay/ca.crt
# inner mTLS material: from <config-dir>/tls (after enrolling), or pass --tls-* flags

Choisissez exactement un mode de confiance pour le certificat externe du relais, comme pour le démon :

IndicateurSignification
--relay-ca <pem>Faire confiance à l’autorité de certification (auto-signée) du relais
--relay-pin <sha256>Épinglez la feuille externe du relais (généralement fournie lors de l’enrôlement)
--relay-tofuFaire confiance lors de la première utilisation ; enregistrer le code PIN dans <config-dir>/relay/relay_pin
--relay-insecureIgnorer la vérification externe (uniquement pour le développement/les certificats auto-signés)
(none)Utiliser des racines publiques intégrées (relais public-CA)
--relay-host <name>Remplace le SAN attendu du certificat externe (par défaut, l’hôte indiqué par --relay)

Équivalent de configuration (pour que zerocode fonctionne seul) :

[connection.wss]
relay_url  = "relay.example.com:8443"
relay_node = "<node-id>"

La confiance externe du relais (--relay-ca / --relay-pin / --relay-tofu / --relay-insecure) est fournie par les options ou le code PIN d’inscription mis en cache, et non par les clés [connection.wss].

3c. Connexion directe en priorité, avec relais de secours

Si vous fournissez au client à la fois une adresse directe et un relais, il privilégie le chemin direct et bascule vers le relais, puis effectue une nouvelle sonde et revient au chemin direct :

zerocode --connect wss://<daemon-host>:9781 \
         --relay relay.example.com:8443 --relay-node <node-id>

Paramétrage (dans [connection.wss]) :

CléPar défautSignification
direct_attempts2Tentatives directes avant de recourir au relais
direct_timeout_secs3Délai d’attente par tentative de connexion directe
reprobe_secs30Fréquence de nouvelle vérification pour repasser en mode direct (0 désactive)

En mode relais uniquement (sans --connect/uri), l’URL WSS interne est par défaut wss://127.0.0.1:9781, car le mTLS interne se termine au niveau de l’écouteur de bouclage du démon ; l’adresse du relais est uniquement la cible de connexion TCP.

3d. Inscription via le navigateur par le point d’entrée relais (opt-in, inscription uniquement)

The relay peut servir une page d’inscription pour navigateur (le « frontdoor ») : ouvrez l’URL du relais, saisissez l’identifiant du nœud et un code d’appairage, comparez la chaîne d’authentification courte (short-auth-string) avec la console du démon, et le navigateur reçoit un certificat client.

Elle sert uniquement à l’enrôlement. Il n’existe aucun niveau de session navigateur : une fois l’enrôlement effectué, la page vous remet le certificat et la clé privée et vous oriente vers zerocode. Le plan RPC du daemon exige mTLS, que un navigateur ne peut pas présenter via l’API WebSocket native ; une session navigateur nécessiterait donc soit une implémentation TLS en JavaScript de page (volontairement non livrée, voir ci-dessous), soit un mode de session terminé par un relais sur le daemon. Il s’agit d’une question de conception ouverte, et non d’une fonctionnalité manquante.

Il est désactivé par défaut, car il réduit la garantie de transfert aveugle pour les navigateurs qui l’utilisent, de deux manières distinctes :

  • Le relais est une origine de code de confiance. Un navigateur qui s’inscrit ici exécute le JavaScript d’inscription servi par le relais. Un relais compromis pourrait substituer du code qui divulgue ce que la page traite.
  • Le relais est un acteur à part entière de l’enrôlement. L’endpoint d’enrôlement du daemon parle TLS, et le relais transmet ces octets sans les déchiffrer. Un navigateur ne peut donc pas effectuer l’échange lui-même. Le relais l’effectue donc pour le compte du navigateur : il récupère la CA du daemon, puis envoie la requête POST /enroll épinglée sur cette CA, en y joignant le code d’appairage. Le relais voit le code d’appairage et le certificat émis, et pourrait utiliser un code d’appairage observé pour enrôler un client à lui.

Ce que le relais n’obtient pas, c’est la clé privée : le navigateur génère la paire de clés avec WebCrypto et ne transmet qu’une CSR.

Le short-auth-string fonctionne toujours comme détecteur : un relais qui substitue une autre CA de daemon produit un SAS qui ne correspond pas à la console du daemon, auquel cas arrêtez-vous s’il diffère. Il ne peut pas vous protéger contre un relais auquel vous avez choisi de faire confiance : l’enrôlement via le navigateur par le frontdoor est terminé au niveau du relais, et non de bout en bout.

Les itérations précédentes de ce frontdoor livraient un client TLS 1.3 et X.509 écrit à la main, en JavaScript servi par le relais, afin que le navigateur puisse dialoguer directement avec le démon. Celui-ci est supprimé, et non porté. Une pile TLS non auditée dans du JavaScript servi offre un modèle de confiance pire qu’une terminaison par le relais, honnêtement divulguée, et elle ne supprimait de toute façon pas le problème de l’origine de confiance du code.

La restriction est bornée : le plan RPC reste en mTLS de bout en bout au-delà du relais dans tous les modes, et zerocode/l’enrôlement natif n’exécute jamais de code servi par le relais et n’est jamais terminé par le relais - utilisez-les (sections 1b/1c/3a) lorsque le relais doit rester entièrement non fiable.

# relay.toml
[frontdoor]
enabled = true   # default false; read the trust note above first

When disabled, plain HTTP requests to the relay get a 404 and browsers cannot enroll through it; daemon registration, zerocode clients, and the tunneled RPC plane are unaffected. The enrollment page itself carries the same trust note, and zerorelay logs a warning at startup while the frontdoor is on.

Wait, that was the source text repeated back, which isn’t a translation. Here is the French version:

Lorsqu’il est désactivé, les requêtes HTTP simples adressées au relais reçoivent un 404 et les navigateurs ne peuvent pas s’enregistrer par son intermédiaire ; l’enregistrement des démons, les clients zerocode et le plan RPC tunnelisé ne sont pas affectés. La page d’inscription elle-même comporte la même note de confiance, et zerorelay journalise un avertissement au démarrage tant que le frontdoor est activé.

Liens d’inscription pré-remplis

Pour éviter à un utilisateur de téléphone de saisir un long identifiant de nœud, un lien peut contenir l’identifiant du nœud et le code d’appairage, et la page remplit les deux champs au chargement.

https://relay.example.com/#node=<node-id>&code=<pairing-code>
https://relay.example.com/?node=<node-id>&code=<pairing-code>

Privilégiez la forme #. Un fragment d’URL n’est jamais envoyé à un serveur, de sorte que le code d’appairage reste hors des journaux d’accès des proxys inverses et des CDN ; la forme ? est également acceptée. Le relais supprime lui-même la chaîne de requête avant le routage, ne la journalise ni ne la renvoie, et sert la page avec cache-control: no-store et referrer-policy: no-referrer.

La page remplit uniquement les champs. Elle ne récupère pas la CA de l’agent et n’envoie rien : l’utilisateur doit toujours appuyer sur Récupérer la CA de l’agent et confirmer la short-auth-string. Les valeurs qui ne correspondent pas au format de node-id ou de pairing-code sont ignorées. Dès que la page les lit, les deux paramètres sont supprimés de la barre d’adresse et de l’entrée d’historique en cours, afin qu’une URL copiée ou mise en favori ne contienne pas le code.

Le navigateur peut tout de même enregistrer le lien tel qu’il a été ouvert la première fois dans sa propre base de données d’historique (Chrome le fait, pour les formes ? et #) ; aucune page ne peut réécrire cela. Ce qui rend cette copie inoffensive, c’est le code lui-même : il est consommé par la première inscription réussie et expire dix minutes après sa génération, de sorte que le lien enregistré est soit épuisé, soit périmé. Traitez un lien contenant un code non utilisé comme le code lui-même, et privilégiez une inscription rapide après la génération.


Référence de configuration

Démon [wss]

CléPar défautDescription
enabledfalseActiver l’écouteur WSS avec TLS mutuel
bind0.0.0.0Adresse de liaison
port9781Port d’écoute
cert_path(vide)Certificat de serveur fourni par l’utilisateur ; s’il est vide, un certificat est généré automatiquement sous <data_dir>/tls/
key_path(vide)Clé de serveur fournie par l’utilisateur ; si vide, génération automatique

Démon [wss.client_auth] (facultatif ; mTLS est toujours activé, quoi qu’il en soit)

CléPar défautDescription
enabledfalseUtilise une autorité de certification fournie par l’utilisateur ; lorsque la valeur est false, le démon utilise son autorité de certification générée automatiquement
ca_cert_path(vide)Autorité de certification PEM utilisée pour vérifier les certificats client (mode BYO)
pinned_certs[]Si elle n’est pas vide, seuls les certificats clients correspondant à ces empreintes SHA-256 sont acceptés
crl_path(vide)Fichier d’empreintes révoquées ; une valeur vide utilise <data_dir>/tls/revoked matérialisé par le registre

Démon [enroll]

CléPar défautDescription
enabledfalseActiver le point de terminaison d’inscription (nécessite [wss] + une clé CA)
bind0.0.0.0Adresse de liaison
port9782Port d’écoute
allow_unpaired_enrollment(vide)Réservé ; les valeurs non vides sont rejetées jusqu’à ce qu’une ancre de confiance client sans code existe

Démon [relay]

CléPar défautDescription
enabledfalseActiver le pont relais (nécessite [wss])
url(vide)Adresse du relais host:port ; obligatoire lorsqu’il est activé
node_id(vide)Si vide, génère automatiquement et conserve un identifiant de 128 bits ; renseignez-en un pour en figer un.
token(vide, secret)Secret partagé présenté lors de l’inscription
relay_ca_path(vide)CA au format PEM pour le certificat externe du relais ; vide, utilise les autorités racines publiques
relay_host(vide)SAN attendu du certificat externe ; vide, dérivé de url
relay_insecurefalseIgnorer la vérification du certificat externe (développement uniquement)
tofufalseÉpingler la feuille de relais lors de la première utilisation dans <data_dir>/relay/relay_pin
outer_client_cert(vide)Certificat client mTLS externe du démon pour l’admission du relais
outer_client_key(vide)Clé pour outer_client_cert
node_id_rotation_days0Renouveler automatiquement le node-id généré tous les N jours (0 = jamais)

Relay relay.toml

Section.keyPar défautDescription
bind0.0.0.0:8443Adresse d’écoute (démon + client)
[tls].cert / .key(auto-provisionnement)Identité TLS externe ; omettez les deux pour un auto-provisionnement
[tls].dirrépertoire de données /tlsOù est enregistré le certificat auto-provisionné
[tls].sans[]SAN supplémentaires (localhost, 127.0.0.1 toujours inclus)
[admission].modeopenopen or allowlist
[admission].allow / .deny[]Empreintes des clés publiques du démon (le refus l’emporte)
[admission].relay_token(none)Contrôle facultatif par secret partagé
[admission].outer_client_authoffoff / optional / required (mTLS externe)
[admission].outer_client_ca(none)CA PEM pour les certificats client externes
[admission].route_by_client_certfalseRouter selon le node-id du CN du certificat externe
[limits].max_conns_per_node256Connexions simultanées des clients par node-id
[limits].idle_timeout_secs300Fermer les connexions client inactives après N secondes
[limits].lease_ttl_secs300TTL du bail communiqué lors de l’enregistrement (à titre indicatif dans la v1 : la vivacité de WebSocket est la véritable règle de nettoyage)
[limits].accept_burst_per_ip / accept_rate_per_ip30 / 10.0Seau de jetons de négociation par adresse IP
[limits].connect_burst_per_node / connect_rate_per_node60 / 20.0Seau de jetons de connexion par nœud
[frontdoor].enabledfalseServir la page d’enrôlement du navigateur (voir 3d : fait du relais une origine de code de confiance ET un principal dans l’enrôlement du navigateur)
[limits].max_pending_handshakes256Sockets ayant passé l’étape d’acceptation mais pas encore classifiées
[limits].handshake_timeout_secs10Une seule échéance pour TLS, la mise à niveau WS et l’enregistrement signé
[limits].max_registered_nodes1024Démons enregistrés simultanément (N+1 reçoit registry_full)

CLI zerorelay (remplace relay.toml)

--config --bind --tls-cert --tls-key --tls-dir --tls-san (répétable) --registration-mode --allow (répétable) --deny (répétable) --relay-token --max-conns-per-node --idle-timeout-secs --lease-ttl-secs --status-file --frontdoor (optionnel ; voir 3d pour les implications en matière de confiance). Sous-commandes : healthcheck [--addr 127.0.0.1:8443], status --file <path>.

zerocode [connection.wss] et CLI

clé [connection.wss]Par défautSurcharge via la CLI
uri(none)--connect
relay_url(none)--relay (nécessite --relay-node)
relay_node(none)--relay-node (nécessite --relay)
direct_attempts2-
direct_timeout_secs3-
reprobe_secs30-
auth_token(none)ZEROCLAW_AUTH_TOKEN env (prioritaire sur les deux clés de jeton)
auth_token_file(none)(prioritaire sur auth_token ; doit être réservé au propriétaire)
auth_providernative (jeton d’appairage)- (p. ex. oidc.<alias>)
clé [connection.wss.tls]Par défautSurcharge via la CLI
ca_cert_path<config-dir>/tls/ca.crt--tls-ca-cert
client_cert_path<config-dir>/tls/client.crt--tls-client-cert (clé requise)
client_key_path<config-dir>/tls/client.key--tls-client-key (certificat requis)
skip_verifyfalse--tls-skip-verify

La confiance externe du relais et l’enrôlement sont uniquement disponibles via la CLI/le cache : --relay-ca --relay-host --relay-insecure --relay-pin --relay-tofu --relay-client-cert --relay-client-key --enroll --enroll-host --enroll-port.

Organisation des fichiers

Démon <data_dir>/:

tls/ca.crt  tls/ca.key            per-daemon CA (key 0600)
tls/server.crt  tls/server.key    WSS server leaf (key 0600)
tls/ledger.db                     issued-cert ledger (SQLite)
tls/revoked                       revoked fingerprints (handshake-checked)
relay/registration.key            Ed25519 relay identity (0600)
relay/node_id                     auto-minted node-id
relay/relay_pin                   pinned relay outer-leaf fingerprint (TOFU)

Client <config-dir>/ :

tls/client.crt  tls/client.key    client identity (key 0600)
tls/ca.crt                         pinned daemon CA
tls/profile.json                   device_id, not_after, cached relay profile
relay/relay_pin                    relay outer-leaf pin (--relay-tofu)

Relais <tls-dir>/ : ca.crt, server.crt, server.key (auto-provisionnés).

Mise à niveau d’un registre existant de certificats émis

tls/ledger.db contient une version de schéma. Un registre écrit par une version antérieure est reconstruit sur place à la première ouverture par le nouveau démon, en conservant chaque certificat émis et révoqué, ainsi que son identifiant d’appareil, sa période de validité et l’auteur de l’audit. La reconstruction s’effectue dans une transaction unique : si elle ne peut pas aboutir, le démon refuse de démarrer et laisse le registre d’origine intact au lieu de le migrer partiellement, et l’erreur indique le nom du fichier. Aucune intervention de l’opérateur n’est requise et les appareils enrôlés n’ont pas besoin d’être réenrôlés.

Certificats non délivrés

Le démon consigne une émission dans le registre avant de remettre le certificat au client en cours d’inscription, à un client qui renouvelle son certificat ou dans les fichiers issue-client-cert de l’opérateur. Cet ordre est délibéré : l’alternative peut laisser un certificat actif signé par l’AC entre les mains de quelqu’un sans aucune entrée dans le registre, et un certificat qui n’y figure pas ne peut être listé ni révoqué.

Le coût est qu’une défaillance intermédiaire — un client qui se déconnecte en cours de réponse, un échec de renommage — laisse une ligne active pour un certificat que personne n’a reçu. Une telle ligne est suivie séparément comme non remise et est révoquée dès qu’elle a plus d’une heure, au premier des événements suivants :

  • toute nouvelle émission ou tout renouvellement de certificat,
  • toute connexion d’enrôlement,
  • tout redémarrage du démon ou toute autre ouverture du registre.

Ce n’est pas un minuteur en arrière-plan. Un daemon qui n’effectue absolument aucune opération sur les certificats reporte le balayage jusqu’à sa prochaine activité de ce type ; jusque-là, le certificat reste absent de tls/revoked et réussirait toujours une négociation WSS, bien qu’il ne puisse pas être renouvelé. Tout trafic d’enrôlement ou de renouvellement — y compris une connexion qui échoue à s’authentifier — suffit à le réconcilier.

Les certificats réconciliés sont révoqués, jamais supprimés : la ligne reste dans le registre et l’empreinte est ajoutée à tls/revoked, exactement comme lors d’une révocation par un opérateur, avec l’acteur d’audit reconcile:undelivered, afin que la trace permette de les distinguer. security list-client-certs n’affiche que les certificats ACTIVE ; un certificat réconcilié disparaît donc de cette liste. Lors d’une réponse à incident, consultez tls/revoked ou le journal d’audit pour connaître l’historique des révocations. Si un appareil signale qu’un certificat qu’il a effectivement reçu ne fonctionne plus, recherchez cet acteur : cela signifie que l’enregistrement de livraison a été perdu, et la solution consiste à réinscrire l’appareil ou à réémettre le certificat.

Vérification et dépannage

  • Démon démarré : recherchez le journal de l’écouteur WSS sur 0.0.0.0:9781 et, avec un relais, la ligne de journal node_id.
  • Connexion sans certificat refusée : comportement attendu sur le plan mTLS — effectuez d’abord l’inscription (Topologie 1b). Le message indique la marche à suivre, ce n’est pas un blocage.
  • Relais accessible : zerorelay healthcheck --addr <host>:8443 se termine avec le code de sortie 0.
  • Métriques Relay : exécutez avec --status-file <path>, puis zerorelay status --file <path> (comptages uniquement, jamais de charges utiles).
  • Révoquer un appareil perdu : la révocation dans le registre du démon matérialise <data_dir>/tls/revoked ; le certificat est refusé lors de sa prochaine négociation et ne peut pas se renouveler automatiquement.
  • Incompatibilité SAS lors de l’inscription : le client refuse de conserver le certificat et abandonne. Une incompatibilité signifie que l’autorité de certification reçue n’est pas celle du démon ; recherchez une éventuelle attaque de l’homme du milieu avant de réessayer.

Il existe un banc de test de bout en bout autonome dans scripts/dev/mtls-relay-testbed.sh qui démarre un démon et un relais, émet un certificat, effectue une inscription via le réseau et teste les trois topologies ; consultez-le comme un exemple concret.