Transporte seguro: configuración de extremo a extremo
Esta página es la referencia completa de configuración para conectar un cliente a un demonio de forma segura, en tres topologías:
- Cliente directo al daemon - WSS con TLS mutuo, sin relé.
- Demonio a relay: el demonio mantiene un puente saliente hacia un relay designado para que sea accesible desde detrás de NAT/CGNAT.
- Del cliente al relé y del relé al demonio: el cliente llega al demonio a través de ese relé, mientras que el mTLS real entre cliente<->demonio sigue terminando en el demonio.
Para la guía de inicio rápido de 60 segundos, consulta Configuración remota (WSS); esta página es la guía más detallada, parámetro por parámetro.
Modelo mental: dos sobres, un límite de confianza
Hay dos capas de TLS, y solo una de ellas es el límite de seguridad:
- Inner mTLS (la frontera real). Solo TLS 1.3, con autenticación mutua. El cliente presenta un certificado emitido por el daemon; el daemon presenta su certificado de hoja del servidor. Este es el plano RPC. No existe ninguna ruta solo del servidor ni sin autenticar en este plano: siempre se requiere un certificado de cliente. El certificado admite el dispositivo; no es una credencial RPC. Cada
initializeremoto también debe incluir un token bearerauth_tokenque identifique al principal, o el daemon lo rechaza conAUTH_REQUIRED(-32010). Ver Bearer token. - TLS externo (un límite de metadatos). Cuando hay un relay en la ruta, el relay termina una sesión externa de TLS + WebSocket y reenvía texto cifrado opaco. Nunca contiene una clave que pueda leer el RPC interno. En la topología directa no hay ninguna capa externa.
Puertos predeterminados (todos configurables):
| Plano | Predeterminado | Config |
|---|---|---|
| Demonio WSS (RPC mTLS interno) | 9781 | [wss].port |
| Punto de conexión de inscripción del demonio | 9782 | [enroll].port |
| Relay (TLS externo + WS) | 8443 | retransmitir --bind / [bind] |
En todo el documento, <data_dir> es el directorio de datos del demonio (normalmente ~/.zeroclaw) y <config-dir> es el directorio de configuración de zerocode del cliente (--config-dir, normalmente ~/.zeroclaw). Los archivos de configuración no expanden ~; usa rutas absolutas.
Topología 1: cliente directo al demonio
zerocode ===== mutual-TLS WSS (TLS 1.3) =====> daemon [wss] :9781
1a. Lado del demonio
Habilita el listener WSS. La opción segura predeterminada es dejar que el demonio genere automáticamente su propia CA y su certificado de servidor durante el primer arranque, para que no tengas que gestionar manualmente ningún material TLS:
[wss]
enabled = true
# bind = "0.0.0.0" # predeterminado
# port = 9781 # predeterminado
# Deja cert_path/key_path vacíos para generar automáticamente un certificado de servidor en
# <data_dir>/tls/ durante el primer arranque. Configúralos solo para usar tu propio certificado de servidor.
En el primer inicio con [wss].enabled = true, el demonio escribe, en <data_dir>/tls/ (directorio con modo 0700):
| Archivo | Propósito | Modo |
|---|---|---|
ca.crt | Certificado de CA por demonio (público) | umask predeterminada |
ca.key | Clave privada de la CA (firma certificados de cliente) | 0600 |
server.crt | Certificado hoja del servidor WSS (SANs localhost, 127.0.0.1) | umask predeterminada |
server.key | clave privada del servidor WSS | 0600 |
Las claves privadas se escriben con permisos 0600; los certificados públicos usan la umask del proceso. El directorio tls/ en sí tiene permisos 0700.
La CA nunca se rota silenciosamente: si existen ca.crt y ca.key, se reutilizan. La duración de la CA generada automáticamente es de 10 años; el certificado hoja del servidor es de ~27 meses; los certificados de cliente emitidos son de 30 días.
Abre el puerto (sudo ufw allow 9781/tcp) e inicia el demonio. Deberías ver una línea de registro que indique que el escuchador WSS está activo en 0.0.0.0:9781.
Ahora el cliente necesita un certificado. Hay dos formas de obtenerlo.
1b. Lado del cliente - opción A: inscripción (recomendada)
La inscripción proporciona a un cliente sin certificado su primer certificado a través de un punto de conexión autenticado por el servidor y protegido mediante emparejamiento, sin gestionar certificados manualmente. Actívala:
[enroll]
enabled = true
# bind = "0.0.0.0" # predeterminado
# port = 9782 # predeterminado
# Requiere [wss] habilitado y una clave de CA del daemon (generada automáticamente arriba o BYO+key).
# Si falta la clave de CA, el endpoint falla de forma segura y los certificados deben aprovisionarse
# por un canal externo.
El demonio imprime un código de emparejamiento de un solo uso y una cadena de autenticación corta (SAS) en su consola/registro al iniciarse. El código es de un solo uso y caduca 10 minutos después de generarse; es la única credencial de tipo portador para la emisión de certificados y aparece en consolas y registros, por lo que un código copiado debe dejar de funcionar poco después de que el operador lo haya utilizado. Un código caducado se rechaza y se elimina; genera un reemplazo mediante la API de emparejamiento de la puerta de enlace cuando necesites uno nuevo. Luego, en la estación de trabajo:
# Interactivo: un cliente sin certificado se registra automáticamente en la primera conexión.
zerocode --connect wss://<remote-host>:9781
# O explícitamente / de forma no interactiva:
zerocode --enroll --connect wss://<remote-host>:9781
zerocode solicita el código de emparejamiento, genera localmente una clave P-256 y una CSR (la clave privada nunca sale del dispositivo) y muestra el SAS. Confirma que el SAS coincide con el que imprimió el demonio (esto detecta una CA de intermediario) y almacena en caché, en <config-dir>/tls/:
| Archivo | Propósito | Modo |
|---|---|---|
client.crt | Certificado de cliente emitido | umask predeterminada |
client.key | Clave privada del cliente (generada localmente) | 0600 |
ca.crt | Cadena de CA del demonio, fijada para el plano RPC | umask predeterminada |
profile.json | device_id, not_after y perfil de retransmisión almacenados en caché | umask predeterminada |
Las ejecuciones posteriores reutilizan el certificado en caché sin volver a registrarse. El certificado se renueva automáticamente al ~50 % de su vida útil (~15 días) sobre la sesión mTLS activa; un certificado revocado no puede renovarse por sí mismo.
Enrollment emite solo un certificado, no un token bearer. Antes de que el daemon acepte una sesión, empareja con el gateway para obtener un token zc_... y entrégalo a zerocode mediante ZEROCLAW_AUTH_TOKEN, auth_token_file o auth_token (ver Bearer token). Con el token configurado, zerocode --connect wss://<remote-host>:9781 (o simplemente zerocode si uri está en la configuración) no necesita más flags.
Valores predeterminados del punto de conexión de inscripción: --enroll-host usa de forma predeterminada el host de --connect; --enroll-port usa de forma predeterminada 9782.
La primera versión requiere intencionadamente un código de emparejamiento para cada inscripción. La opción reservada allow_unpaired_enrollment se rechaza durante el inicio del demonio hasta que el cliente disponga de un ancla de confianza explícita que no requiera código, como una huella digital fijada de la CA del demonio. Esto evita que la ruta TLS de inscripción provisional se convierta en una confianza ciega en el primer uso.
1c. Lado del cliente - opción B: certificado emitido por el operador
Si prefiere generar un certificado en el daemon y copiarlo fuera:
# En el host del demonio. --out-dir también escribe un ca.crt/client.crt/client.key de sustitución directa.
zeroclaw security issue-client-cert --name my-laptop --out-dir /tmp/my-laptop-tls
# añade --force para sobrescribir un certificado existente con este nombre
Copia los tres archivos en <config-dir>/tls/ del cliente con los nombres ca.crt, client.crt y client.key (entonces zerocode --connect wss://host:9781 los encontrará automáticamente), o indica sus rutas explícitamente:
zerocode --connect wss://<remote-host>:9781 \
--tls-ca-cert /path/ca.crt \
--tls-client-cert /path/client.crt \
--tls-client-key /path/client.key
Configuración equivalente (para que zerocode funcione directamente):
[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"
El zerocode simple aún necesita un token bearer, ya sea desde ZEROCLAW_AUTH_TOKEN o desde auth_token_file / auth_token en [connection.wss].
Un cliente sin certificado que llega al plano WSS sin inscribirse recibe un mensaje accionable de «inscríbete primero» (y el daemon registra el cliente no migrado rechazado); nunca se queda bloqueado silenciosamente.
--tls-skip-verifysolo relaja la verificación del servidor para un daemon de desarrollo autofirmado; el certificado del cliente sigue siendo obligatorio.
Topología 2: del demonio al relé
daemon ====== outbound: register + bridge ======> relay :8443
[relay] (blind forwarder)
El demonio se conecta al relé, demuestra una identidad Ed25519 estable y registra un node-id. Posteriormente, los clientes se conectan a ese node-id (Topología 3). El relé solo reenvía texto cifrado.
2a. Ejecuta el relay (zerorelay)
Configura con relay.toml (consulta apps/zerorelay/relay.example.toml); cada opción de CLI anula el valor correspondiente del archivo. La sección [admission] se recarga en caliente con SIGHUP; se rechaza una recarga que convertiría un relay público en una admisión abierta sin token sin la activación explícita, y la política anterior sigue activa.
# relay.toml
bind = "0.0.0.0:8443"
[tls]
# Omita cert/key para AUTOPROVISIONAR un certificado TLS externo en dir en el primer inicio (sin
# openssl). Establezca sans con el/los nombre(s) de host o IP(s) públicos del relay.
dir = "/data/tls"
sans = ["relay.example.com"]
# O use su propio certificado (p. ej., uno de una CA pública):
# cert = "/etc/zerorelay/fullchain.pem"
# key = "/etc/zerorelay/privkey.pem"
[admission]
# "open" admite cualquier daemon firmado (sujeto a la lista de deny); "allowlist" admite
# solo las huellas de clave pública de daemon incluidas en la lista. Deny siempre tiene prioridad.
mode = "open"
allow = []
deny = []
# Un relay público (no loopback) DEBE controlar el registro: establezca aquí un secreto compartido
# (cada daemon lo presenta mediante [relay] token) o use mode = "allowlist".
# Un relay que atiende daemons registrados con `zeroclaw relay claim` debe usar
# mode = "allowlist" y dejar relay_token sin definir (ver 2d).
# De lo contrario, un relay ABIERTO y sin token en un bind público se niega a iniciar, porque
# cualquier daemon en Internet podría registrarse y acaparar node-ids no reclamados. (Un bind
# de loopback para desarrollo local está exento; un relay público abierto deliberado puede
# anular esto con 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
Ejecútalo:
zerorelay --config /etc/zerorelay/relay.toml
# de forma equivalente, solo opciones (un enlace público necesita un token o una lista de permitidos; de lo contrario, el
# 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
Cuando se omiten --tls-cert/--tls-key, el relay aprovisiona automáticamente una CA + un certificado de servidor en el directorio de TLS (orden de resolución: $ZERORELAY_DATA_DIR/tls, luego $HOME/.zerorelay/tls y, por último, ./zerorelay/tls); localhost y 127.0.0.1 siempre se incluyen en los SAN. El ca.crt aprovisionado automáticamente es en el que confían un demonio/cliente para el TLS externo del relay.
Control de acceso. El modo open, junto con un relay_token opcional, es el mecanismo de control de acceso más sencillo. El modo allowlist usa como clave la huella digital de la clave pública de registro del demonio (el hexadecimal SHA-256 de la clave Ed25519 en <data_dir>/relay/registration.key); añade huellas digitales a allow (y vuelve a cargar con kill -HUP <pid>). Un ID de nodo queda vinculado a la clave pública de su primer registrante, por lo que una clave diferente no puede secuestrar un ID de nodo activo (obtiene node_taken).
El relay comprueba relay_token antes de la lista de permitidos, en todos los modos. Por lo tanto, un relay que atiende reclamaciones de autoservicio (2d) debe usar allowlist con relay_token sin definir: una reclamación nunca entrega el token compartido, así que un daemon reclamado sería rechazado con forbidden: bad relay token. La lista de permitidos ya controla por sí sola un relay público, así que el token no aporta nada en ese caso.
Docker. apps/zerorelay/Dockerfile ejecuta distroless con CMD ["--config", "/etc/zerorelay/relay.toml"] y un HEALTHCHECK sin shell zerorelay healthcheck --addr; compose.yaml monta un volumen en /data para que el TLS autoprovisionado persista. Expón 8443.
2b. Dirige el demonio al relé
[wss]
enabled = true # OBLIGATORIO: el relay reenvía al listener WSS local
[relay]
enabled = true
url = "relay.example.com:8443"
# node_id: dejar VACÍO (recomendado) para generar automáticamente + conservar una
# capacidad aleatoria de 128 bits en <data_dir>/relay/node_id. Establecerlo solo para fijar un id específico.
# token = "change-me" # debe coincidir con [admission].relay_token del relay, si se establece
# Confianza en el certificado EXTERIOR del relay: elegir UNO:
relay_ca_path = "/path/to/relay/ca.crt" # confiar en la CA (autofirmada) del relay
# tofu = true # O fijar el certificado hoja del relay en el primer uso
# relay_insecure = true # O omitir la verificación exterior (solo para desarrollo)
# (dejar los tres sin establecer para usar las raíces públicas integradas, para un relay con CA pública)
[relay] requiere que [wss] esté habilitado (el relay actúa como puente hacia 127.0.0.1:<wss.port>) y falla de forma segura si url está vacío. Al iniciarse, el demonio registra el identificador del nodo con la indicación “proporcione este valor a los clientes como –relay-node”; también puede leerlo de <data_dir>/relay/node_id. La clave de registro estable del demonio se crea en <data_dir>/relay/registration.key (0600).
Precedencia de confianza del certificado externo (de mayor a menor): relay_insecure > relay_ca_path > un pin almacenado en <data_dir>/relay/relay_pin (explícito o TOFU) > tofu > raíces públicas. Por tanto, configurar una CA reemplaza un pin obsoleto sin eliminarlo. Con tofu = true, la huella digital observada del certificado hoja del relay se fija en <data_dir>/relay/relay_pin, y el registro entrega ese mismo pin a los clientes para que fijen la misma hoja.
2c. (opcional) rotación de node-id y mTLS externo
[relay]
node_id_rotation_days = 30 # auto-rotate the auto-minted id every N days (0 = never)
La rotación genera un identificador nuevo, lo mantiene activo junto al anterior durante un periodo de gracia de 10 minutos para que los clientes con operaciones en curso no se desconecten y, después, retira el identificador antiguo; el nuevo identificador llega a los clientes en banda durante su próxima renovación del certificado. Fuerza una ahora con zeroclaw security relay-rotate-node-id (solo en el modo auto-mint; un node_id fijado nunca se rota).
Para un relay que también autentica demonios en la capa externa, configura en el relay [admission].outer_client_auth = "required" + outer_client_ca, y en el demonio [relay].outer_client_cert / outer_client_key. Esto se añade al TLS externo y nunca afecta al mTLS interno.
2d. Inscripción autoservicio con zeroclaw relay claim
Cuando el relay se ejecuta junto con un plano de control de ZeroRelay, un operador puede registrar un daemon con un token de reclamación de un solo uso en lugar de editar manualmente la lista de permitidos del relay. El plano de control emite el token e imprime el comando que se debe ejecutar en el host del daemon:
zeroclaw relay claim <TOKEN> --control https://control.example.com
El demonio firma la afirmación con su clave de registro existente, por lo que la huella digital que el plano de control incluye en la lista de permitidos es exactamente la clave con la que el demonio se registra. Tras un éxito verificado, el plano de control añade esa huella digital a la lista de permitidos del relé y recarga el relé, y el demonio escribe [relay] enabled, url, node_id y relay_host. Inicie o reinicie el demonio para registrarse.
Antes de reclamar:
- El relay debe admitir solo por huella digital. Usa
mode = "allowlist"y dejarelay_tokensin definir (ver 2a). Una afirmación no entrega el token compartido. - Habilite
[wss]. El relé rechaza el registro hasta que el listener WSS esté habilitado. Una reclamación realizada sin ello se guarda igualmente y surte efecto en el siguiente inicio tras habilitar[wss]. - Configure primero la confianza para el certificado externo del relay, como en 2b (
relay_ca_path,tofuo raíces públicas). El claim escribe solo los cuatro campos anteriores y deja el resto de [relay] intacto. - No configures los campos gestionados por la reclamación mediante variables de entorno de anulación. Si se anula
relay.enabled,relay.url,relay.node_idorelay.relay_host, la reclamación se rechaza antes de enviar nada, porque la anulación descartaría los valores escritos.
--control debe ser https, o http en una dirección de loopback. Las redirecciones y los proxies están deshabilitados para que el token no pueda llegar a otro origen. Cada rechazo indica no config was written. Un token es de un solo uso: una vez que el plano de control lo acepta, queda consumido, incluso si el daemon rechaza después la respuesta (por ejemplo, por una dirección de relay no utilizable). En ese caso, corrija la causa y genere un nuevo token.
Topología 3: del cliente al relé y del relé al demonio
zerocode ==outer TLS+WS==> relay ==forwards ciphertext==> daemon
\________________ inner mutual-TLS (TLS 1.3) terminates here _______________/
Esto combina las topologías 1 y 2: el cliente necesita un certificado de cliente interno (registro, como en 1b) y las coordenadas del relay (dirección, node-id y confianza en el certificado externo del relay).
3a. La opción sencilla: el registro incluye el perfil de retransmisión
Cuando el demonio tiene [relay] configurado, su respuesta de inscripción incluye un perfil de relay (relay_url, node_id y el relay_cert_pin de hoja del relay). Por lo tanto, una sola inscripción aprovisiona todo:
zerocode --enroll --connect wss://<daemon-host>:9781
zerocode almacena en caché el certificado interno y el perfil del relay en <config-dir>/tls/profile.json. Más tarde, un zerocode sin opciones llega al demonio a través del relay; ya conoce la dirección del relay, el node-id y el PIN.
3b. La ruta manual
Proporcione explícitamente al cliente las coordenadas del relé. El certificado interno sigue procediendo de la inscripción o de --tls-* (Topología 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
Elige exactamente un modo de confianza para el certificado externo del relé, igual que en el demonio:
| Bandera | Significado |
|---|---|
--relay-ca <pem> | Confía en la CA (autofirmada) del relé |
--relay-pin <sha256> | Fija la hoja externa del relé (normalmente entregada durante la inscripción) |
--relay-tofu | Confía en el primer uso; guarda el pin en <config-dir>/relay/relay_pin |
--relay-insecure | Omitir la verificación externa (solo para desarrollo/certificados autofirmados) |
| (ninguno) | Usa las raíces públicas integradas (retransmisión de CA pública) |
--relay-host <name> | Sobrescribe el SAN esperado del certificado externo (de forma predeterminada, el host de --relay) |
Equivalente de configuración (para que zerocode funcione sin más):
[connection.wss]
relay_url = "relay.example.com:8443"
relay_node = "<node-id>"
La confianza externa del relay (
--relay-ca/--relay-pin/--relay-tofu/--relay-insecure) se proporciona mediante las opciones o el PIN de inscripción almacenado en caché, no mediante las claves de[connection.wss].
3c. Directo primero con respaldo de retransmisión
Si proporciona al cliente tanto una dirección directa como un relay, este prefiere la ruta directa y recurre al relay; después vuelve a sondear y migra de nuevo:
zerocode --connect wss://<daemon-host>:9781 \
--relay relay.example.com:8443 --relay-node <node-id>
Ajuste (en [connection.wss]):
| Clave | Predeterminado | Significado |
|---|---|---|
direct_attempts | 2 | Intentos directos antes de recurrir al relé |
direct_timeout_secs | 3 | Tiempo de espera por intento de conexión directa |
reprobe_secs | 30 | Intervalo de re-sondeo para volver al modo directo (0 desactiva) |
En el modo de solo retransmisión (sin --connect/uri), la URL WSS interna se establece de forma predeterminada en wss://127.0.0.1:9781 porque el mTLS interno termina en el listener de bucle invertido del demonio; la dirección de retransmisión solo es el destino de conexión TCP.
3d. Inscripción desde el navegador a través de la puerta de enlace del relay (opt-in, solo inscripción)
El relay puede servir una página de inscripción en el navegador (la “frontdoor”): abre la URL del relay, introduce el ID del nodo y un código de emparejamiento, compara la short-auth-string con la consola del daemon y el navegador recibe un certificado de cliente.
Solo realiza el registro. No existe un nivel de sesión en el navegador: una vez registrado, la página te entrega el certificado y la clave privada y te indica usar zerocode. El plano RPC del daemon requiere mTLS, que un navegador no puede presentar a través de la API nativa de WebSocket; por lo tanto, una sesión en el navegador necesitaría una implementación de TLS en JavaScript de la página (deliberadamente no incluida, ver más abajo) o un modo de sesión terminada en un relay en el daemon. Se trata de una pregunta de diseño abierta, no de una funcionalidad faltante.
Está desactivado por defecto, porque reduce la garantía de reenvío ciego para los navegadores que lo usan, de dos maneras distintas:
- El relay es un origen de código de confianza. Un navegador que se inscribe aquí ejecuta JavaScript de inscripción servido por el relay. Un relay comprometido podría sustituir código que filtre lo que maneja la página.
- El relay es un principal en la inscripción. El endpoint de inscripción del daemon usa TLS, y el relay reenvía esos bytes sin descifrarlos, por lo que un navegador no puede realizar el intercambio por sí mismo. Por tanto, el relay lo realiza en nombre del navegador: obtiene la CA del daemon y luego envía un
POST /enrollfijado a la CA, que incluye el código de emparejamiento. El relay ve el código de emparejamiento y el certificado emitido, y podría usar un código de emparejamiento observado para inscribir un cliente propio.
Lo que el relay no recibe es la clave privada: el navegador genera el par de claves con WebCrypto y transmite únicamente una CSR.
El short-auth-string sigue funcionando como detector: un relay que sustituya la CA de un daemon distinto produce una SAS que no coincide con la consola del daemon, así que detente si difiere. No puede protegerte frente a un relay en el que hayas decidido confiar: el enrolamiento desde el navegador a través del frontdoor es terminado en el relay, no de extremo a extremo.
Las versiones anteriores de este frontdoor incluían un cliente TLS 1.3 y X.509 escrito a mano, servido como JavaScript desde el relay, para que el navegador pudiera comunicarse directamente con el daemon. Eso está eliminado, no portado. Un stack TLS no auditado en JavaScript servido ofrece una postura de confianza peor que un esquema con terminación en el relay, declarada con honestidad, y de todos modos no resolvía el problema del origen de confianza del código.
La reducción está acotada: el plano RPC se mantiene como mTLS de extremo a extremo más allá del relay en todos los modos, y zerocode/la inscripción nativa nunca ejecuta código servido por el relay ni termina en el relay. Usa esas opciones (secciones 1b/1c/3a) cuando el relay deba permanecer completamente no confiable.
# relay.toml
[frontdoor]
enabled = true # default false; read the trust note above first
Cuando está deshabilitado, las solicitudes HTTP simples al relay reciben un 404 y los navegadores no pueden inscribirse a través de él; el registro del daemon, los clientes zerocode y el plano RPC tunelizado no se ven afectados. La propia página de inscripción muestra la misma nota de confianza, y zerorelay registra una advertencia al iniciar mientras el frontdoor está activado.
Enlaces de inscripción prellenados
Para evitarle a un usuario de teléfono escribir un ID de nodo largo, un enlace puede incluir el ID de nodo y el código de emparejamiento, y la página completa ambos campos al cargarse:
https://relay.example.com/#node=<node-id>&code=<pairing-code>
https://relay.example.com/?node=<node-id>&code=<pairing-code>
Prefiera la forma #. Un fragmento de URL nunca se envía a ningún servidor, por lo que el código de emparejamiento queda fuera de los registros de acceso de los proxies inversos y las CDN; la forma ? también es aceptada. El relay en sí elimina la cadena de consulta antes de enrutar y nunca la registra ni la refleja, y sirve la página con cache-control: no-store y referrer-policy: no-referrer.
La página solo rellena los campos. No obtiene la CA del agente ni envía nada: el usuario sigue pulsando Obtener la CA del agente y confirma la short-auth-string. Los valores que no tienen el formato de node-id o de pairing-code se ignoran, y ambos parámetros se eliminan de la barra de direcciones y de la entrada actual del historial en cuanto la página los lee, de modo que una URL copiada o guardada en marcadores no contiene el código.
El navegador puede seguir registrando el enlace tal como se abrió por primera vez en su propia base de datos de historial (Chrome lo hace, tanto con la forma ? como con la #); ninguna página puede reescribirlo. Lo que hace inofensiva esa copia es el propio código: se consume con la primera inscripción exitosa y caduca diez minutos después de su emisión, de modo que el enlace almacenado queda agotado o inválido. Trate un enlace que contenga un código sin usar como si fuera el propio código, y procure inscribirse con prontitud después de generarlo.
Referencia de configuración
Demonio [wss]
| Clave | Predeterminado | Descripción |
|---|---|---|
enabled | false | Habilitar el listener WSS de TLS mutuo |
bind | 0.0.0.0 | Dirección de enlace |
port | 9781 | Puerto de escucha |
cert_path | (vacío) | Certificado de servidor proporcionado por el usuario; si está vacío, se genera automáticamente en <data_dir>/tls/ |
key_path | (vacío) | Clave de servidor propia; si está vacía, se genera automáticamente |
Demonio [wss.client_auth] (opcional; mTLS siempre está activado de todos modos)
| Clave | Predeterminado | Descripción |
|---|---|---|
enabled | false | Usa una CA propia; cuando es false, el demonio usa su CA generada automáticamente |
ca_cert_path | (vacío) | CA en formato PEM utilizada para verificar certificados de cliente (modo BYO) |
pinned_certs | [] | Si no está vacío, solo se aceptan certificados de cliente que coincidan con estas huellas digitales SHA-256 |
crl_path | (vacío) | Archivo de huellas digitales revocadas; si está vacío, usa el <data_dir>/tls/revoked materializado por el registro |
Demonio [enroll]
| Clave | Predeterminado | Descripción |
|---|---|---|
enabled | false | Habilitar el endpoint de inscripción (requiere [wss] + una clave de CA) |
bind | 0.0.0.0 | Dirección de enlace |
port | 9782 | Puerto de escucha |
allow_unpaired_enrollment | (vacío) | Reservado; se rechazan los valores no vacíos hasta que exista un anclaje de confianza del cliente sin código |
Demonio [relay]
| Clave | Predeterminado | Descripción |
|---|---|---|
enabled | false | Habilita el puente de retransmisión (requiere [wss]) |
url | (vacío) | Dirección del relé host:port; obligatoria cuando está habilitado |
node_id | (vacío) | Si está vacío, genera y persiste automáticamente un identificador de 128 bits; establece uno para fijarlo. |
token | (vacío, secreto) | Secreto compartido presentado durante el registro |
relay_ca_path | (vacío) | CA PEM para el certificado externo del relay; vacío usa las raíces públicas |
relay_host | (vacío) | SAN esperado del certificado externo; si está vacío, se deriva de url |
relay_insecure | false | Omitir la verificación del certificado externo (solo para desarrollo) |
tofu | false | Fija la hoja de retransmisión en el primer uso en <data_dir>/relay/relay_pin |
outer_client_cert | (vacío) | Certificado de cliente mTLS externo del daemon para la admisión del relay |
outer_client_key | (vacío) | Clave de outer_client_cert |
node_id_rotation_days | 0 | Rotar automáticamente el node-id generado automáticamente cada N días (0 = nunca) |
Relay relay.toml
| Section.key | Predeterminado | Descripción |
|---|---|---|
bind | 0.0.0.0:8443 | Dirección de escucha (demonio + cliente) |
[tls].cert / .key | (autoaprovisionamiento) | Identidad TLS externa; omita ambas para aprovisionarse automáticamente |
[tls].dir | directorio de datos /tls | Dónde se escribe el certificado autoprovisionado |
[tls].sans | [] | SAN adicionales (localhost, 127.0.0.1 siempre incluidos) |
[admission].mode | open | open o allowlist |
[admission].allow / .deny | [] | Huellas digitales de las claves públicas del demonio (la denegación prevalece) |
[admission].relay_token | (ninguno) | Control de acceso opcional mediante secreto compartido |
[admission].outer_client_auth | off | off / optional / required (mTLS externo) |
[admission].outer_client_ca | (ninguno) | CA en formato PEM para certificados de cliente externos |
[admission].route_by_client_cert | false | Enrutar según el node-id del CN del certificado externo |
[limits].max_conns_per_node | 256 | Conexiones simultáneas de clientes por node-id |
[limits].idle_timeout_secs | 300 | Descarta las conexiones de cliente inactivas después de N segundos |
[limits].lease_ttl_secs | 300 | TTL de la concesión anunciado durante el registro (informativo en v1: la actividad de WebSocket es la regla de limpieza real) |
[limits].accept_burst_per_ip / accept_rate_per_ip | 30 / 10.0 | Cubo de tokens de handshake por IP |
[limits].connect_burst_per_node / connect_rate_per_node | 60 / 20.0 | Cubo de tokens de conexión por nodo |
[frontdoor].enabled | false | Sirve la página de inscripción del navegador (ver 3d: convierte al relay en un origen de código de confianza Y en un principal en la inscripción del navegador) |
[limits].max_pending_handshakes | 256 | Sockets que han pasado por accept pero aún no se han clasificado |
[limits].handshake_timeout_secs | 10 | Un único tiempo límite para TLS, la actualización de WS y el registro firmado |
[limits].max_registered_nodes | 1024 | Daemons registrados simultáneamente (N+1 obtiene registry_full) |
zerorelay CLI (anula relay.toml)
--config --bind --tls-cert --tls-key --tls-dir --tls-san (repetible) --registration-mode --allow (repetible) --deny (repetible) --relay-token --max-conns-per-node --idle-timeout-secs --lease-ttl-secs --status-file --frontdoor (opcional; consulte 3d para las implicaciones de confianza). Subcomandos: healthcheck [--addr 127.0.0.1:8443], status --file <path>.
zerocode [connection.wss] y CLI
clave [connection.wss] | Predeterminado | Anulación de la CLI |
|---|---|---|
uri | (ninguno) | --connect |
relay_url | (ninguno) | --relay (requiere --relay-node) |
relay_node | (ninguno) | --relay-node (requiere --relay) |
direct_attempts | 2 | - |
direct_timeout_secs | 3 | - |
reprobe_secs | 30 | - |
auth_token | (ninguno) | ZEROCLAW_AUTH_TOKEN env (tiene prioridad sobre ambas claves de token) |
auth_token_file | (ninguno) | (tiene prioridad sobre auth_token; debe ser de solo lectura para el propietario) |
auth_provider | native (token de emparejamiento) | - (p. ej. oidc.<alias>) |
clave [connection.wss.tls] | Predeterminado | Anulación de la CLI |
|---|---|---|
ca_cert_path | <config-dir>/tls/ca.crt | --tls-ca-cert |
client_cert_path | <config-dir>/tls/client.crt | --tls-client-cert (requiere una clave) |
client_key_path | <config-dir>/tls/client.key | --tls-client-key (requiere un certificado) |
skip_verify | false | --tls-skip-verify |
La confianza externa y la inscripción de Relay solo están disponibles mediante la CLI/caché: --relay-ca --relay-host --relay-insecure --relay-pin --relay-tofu --relay-client-cert --relay-client-key --enroll --enroll-host --enroll-port.
Diseño de archivos
Demonio <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)
Cliente <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)
Relay <tls-dir>/: ca.crt, server.crt, server.key (autoprovisionados).
Actualización de un registro existente de certificados emitidos
tls/ledger.db incluye una versión del esquema. Un registro escrito por una revisión anterior se reconstruye en el mismo lugar la primera vez que el nuevo demonio lo abre, conservando cada certificado emitido y revocado junto con su identificador de dispositivo, validez y actor de auditoría. La reconstrucción se ejecuta en una única transacción: si no puede completarse, el demonio se niega a iniciarse y deja intacto el registro original en lugar de migrarlo parcialmente, y el error indica el nombre del archivo. No se requiere ninguna acción del operador, y los dispositivos registrados no necesitan volver a registrarse.
Certificados no entregados
El demonio registra una emisión en el libro mayor antes de entregar el certificado: al cliente que se está registrando, al cliente que está renovando o a los archivos issue-client-cert del operador. Ese orden es deliberado: la alternativa puede dejar en manos de alguien un certificado válido firmado por la CA sin una fila correspondiente en el libro mayor, y un certificado del que el libro mayor nunca ha tenido constancia no se puede listar ni revocar.
El coste es que un fallo intermedio - un cliente que se desconecta a mitad de la respuesta, un renombrado fallido - deja un registro active para un certificado que nadie recibió. Este registro se sigue por separado como no entregado y se revoca cuando tiene más de una hora, en el primero de:
- cualquier nueva emisión o renovación de certificados,
- cualquier conexión de inscripción,
- cualquier reinicio del demonio u otra apertura del libro mayor.
No es un temporizador en segundo plano. Un demonio que no realiza ningún trabajo relacionado con certificados aplaza el barrido hasta su siguiente actividad de ese tipo; hasta entonces, el certificado permanece fuera de tls/revoked y aún pasaría la negociación de WSS, aunque no puede renovarse. Cualquier tráfico de inscripción o renovación - incluida una conexión que no logra autenticarse - basta para reconciliarlo.
Los certificados reconciliados se revocan, nunca se eliminan: la fila permanece en el libro mayor y la huella digital se registra en tls/revoked, exactamente igual que en una revocación realizada por un operador, con el actor de auditoría reconcile:undelivered para que el historial las distinga. security list-client-certs muestra únicamente certificados ACTIVE, por lo que un certificado reconciliado desaparece de ese listado - durante la respuesta a incidentes, consulta tls/revoked o el registro de auditoría para conocer el historial de revocaciones. Si un dispositivo informa de que un certificado que sí recibió dejó de funcionar, busca ese actor - significa que se perdió el registro de entrega, y la solución es volver a inscribir el dispositivo o volver a emitir el certificado.
Verificación y solución de problemas
- Demonio activo: busca el registro del listener WSS en
0.0.0.0:9781y, con un relé, la línea de registro denode_id. - Conexión sin certificado rechazada: es lo esperado en el plano de mTLS: regístrate primero (Topología 1b). El mensaje indica qué hacer; no es un bloqueo.
- Relay accesible:
zerorelay healthcheck --addr <host>:8443finaliza con el código 0. - Métricas de Relay: ejecuta con
--status-file <path>, luegozerorelay status --file <path>(solo recuentos, nunca cargas útiles). - Revocar un dispositivo perdido: revocarlo en el registro del demonio crea
<data_dir>/tls/revoked; el certificado se rechaza en su siguiente protocolo de enlace y no puede renovarse automáticamente. - SAS no coincide durante el registro: el cliente se niega a conservar el certificado y cancela. Una discrepancia significa que la CA que recibiste no es la del demonio; investiga un posible ataque de intermediario antes de volver a intentarlo.
Hay un entorno de pruebas autocontenido de extremo a extremo en scripts/dev/mtls-relay-testbed.sh que inicia un demonio y un relé, emite un certificado, realiza la inscripción a través de la red y prueba las tres topologías; léelo como un ejemplo práctico.