# ¿Puede la canonicalización de host SSH cambiar el destino aprobado?

Una aprobación que dice `ssh build` no autoriza necesariamente la máquina que recibe la conexión TCP. OpenSSH puede ampliar ese nombre corto, seguir un CNAME permitido, analizar su configuración por segunda vez, elegir otro usuario o puerto y resolver el nombre resultante en una de varias direcciones. Si una pasarela para agentes aprueba únicamente el texto enviado por el agente, su aprobación puede describir un destino mientras el cliente SSH utiliza otro.

Eso no es un argumento contra la canonicalización. Los nombres cortos son útiles, los nombres canónicos facilitan la gestión de la configuración y las claves de host siguen autenticando a los servidores. Es un argumento a favor de situar el límite de aprobación después de que OpenSSH haya calculado el destino efectivo y antes de que abra un socket. La persona que aprueba la acción debe ver el nombre solicitado, el nombre final y la dirección, junto con cualquier cambio material introducido por la configuración.

## Un comando SSH contiene cuatro identidades distintas

Un destino SSH no es una sola cadena. Tratarlo como tal crea el fallo que analiza este artículo. Una ruta de aprobación seria mantiene separadas cuatro identidades:

- El nombre solicitado es el argumento proporcionado por el agente, por ejemplo `build`.
- El nombre de host efectivo es el valor que OpenSSH utiliza después de la sustitución de `Hostname` y la canonicalización, por ejemplo `build.ops.example`.
- La dirección del par es la dirección IP elegida cuando el cliente abre el socket.
- La identidad autenticada es la clave o el certificado de host aceptado para esa conexión.

Estos valores responden preguntas distintas. El nombre solicitado registra la intención. El nombre efectivo controla las coincidencias posteriores de configuración y, normalmente, la búsqueda de claves de host. La dirección dice adónde fueron los paquetes. La clave de host indica qué servidor demostró poseer una clave privada. Una aprobación necesita los tres primeros datos antes de conectar, mientras que el resultado de auditoría debe añadir el cuarto tras el intercambio de claves.

OpenSSH expone esta diferencia de nombres en `ssh_config`. El token `%n` significa el nombre de host remoto original de la línea de comandos, mientras que `%h` indica el nombre remoto después de procesar la configuración. `%k` es otro valor: `HostKeyAlias` si se ha definido o, en caso contrario, el nombre original de la línea de comandos. Esos tokens existen porque OpenSSH no puede fingir con seguridad que todas las fases comparten el mismo nombre.

La distinción también explica por qué una comprobación correcta de la clave de host no arregla una autorización ambigua. La autenticación puede demostrar que el extremo posee una clave esperada. No puede demostrar que la persona pretendía autorizar ese extremo cuando la tarjeta mostraba solo `build`. La autorización pregunta si esta acción puede alcanzar este destino. La autenticación pregunta si el servidor que responde coincide con un registro de identidad. Ambas comprobaciones importan y ocurren en momentos diferentes.

## La canonicalización puede reescribir el nombre antes de conectar

`CanonicalizeHostname` indica si OpenSSH debe reescribir nombres de forma explícita. Su valor predeterminado es `no`, que deja la resolución al sistema. Con `yes`, OpenSSH prueba para las conexiones directas los sufijos de `CanonicalDomains`. Con `always`, también canonicaliza los destinos alcanzados mediante `ProxyCommand` o `ProxyJump`.

Supongamos que el agente solicita esta acción:

```text
host: build
user: deploy
command: /usr/local/bin/release status
```

El cliente podría tener esta configuración:

```sshconfig
Host *
    CanonicalizeHostname yes
    CanonicalDomains ops.example
    CanonicalizeFallbackLocal no
    CanonicalizeMaxDots 1
    CanonicalizePermittedCNAMEs *.ops.example:*.hosts.example
```

OpenSSH prueba primero `build.ops.example`. Si DNS devuelve un CNAME permitido hacia `build-07.hosts.example`, ese destino puede convertirse en el nombre canónico. La solicitud aún dice `build`, pero el cliente ya no conecta con ese nombre. Una tarjeta que muestra únicamente la solicitud oculta la transformación relevante.

Los límites merecen atención. `CanonicalizeMaxDots` tiene un valor predeterminado de 1, por lo que un nombre con un punto sigue siendo apto para la canonicalización, no solo una etiqueta sin puntos. `CanonicalizeFallbackLocal` vale `yes` de forma predeterminada; si los dominios canónicos configurados no dan resultado, OpenSSH puede entregar el nombre original al resolvedor del sistema y a sus reglas de búsqueda. El valor `no` convierte el fallo en explícito. Para una pasarela de aprobación suele ser mejor, porque una lista de búsqueda local puede variar según la red y el momento.

El seguimiento de CNAME se restringe por separado. `CanonicalizePermittedCNAMEs` vale `none` de forma predeterminada y sus reglas emparejan patrones de origen permitidos con patrones de destino. Es un valor sensato. Una regla amplia como `*:*` convierte los alias DNS en una etapa de reescritura sin restricciones. Una regla estrecha documenta la transición de espacios de nombres que se espera de verdad, por ejemplo desde alias de servicio bajo `ops.example` hasta hosts bajo `hosts.example`.

El manual de OpenSSH describe estos controles con precisión, pero una capa de políticas no debe limitarse a copiar sus valores en una tarjeta. Debe ejecutar el mismo cálculo con la misma configuración del cliente. Volver a implementar las búsquedas de sufijos y las reglas CNAME dentro del servicio de aprobación suele producir diferencias con el cliente SSH a medida que cambian las versiones y la configuración local.

## La segunda lectura cambia más que DNS

Cuando la canonicalización está habilitada, OpenSSH vuelve a procesar su configuración con el nuevo nombre de destino. Esa segunda lectura puede activar bloques `Host` y `Match` que no coincidían con el alias original. Por tanto, una comprobación previa que solo mire DNS puede pasar por alto otros cambios del destino.

Pensemos en un nombre canónico que coincide con este bloque:

```sshconfig
Match canonical host *.prod.ops.example
    User release
    Port 2222
    IdentityFile ~/.ssh/prod_release
    ProxyJump bastion.ops.example
```

Una solicitud para `deploy` podría ampliarse primero a `deploy.prod.ops.example` y adoptar después otro usuario remoto, puerto, archivo de identidad y host de salto. OpenSSH usa el primer valor obtenido para la mayoría de las directivas, de modo que la aplicación de esos ajustes depende de lo que ya hayan definido los bloques anteriores y de su orden. No se puede deducir la configuración final leyendo de forma aislada el bloque que coincide.

La condición `canonical` coincide únicamente cuando se vuelve a analizar la configuración tras la canonicalización. La condición `final` solicita una última lectura aunque la canonicalización esté deshabilitada. Cuando está activa, `canonical` y `final` coinciden durante la misma pasada. `host` compara el destino después de una sustitución mediante `Hostname` o la canonicalización, mientras que `originalhost` compara el valor de la línea de comandos. Estos detalles hacen que una configuración aparentemente sencilla dependa bastante del estado.

El patrón peligroso consiste en aprobar `host=deploy, user=agent, port=22`, entregar después `ssh deploy` a un cliente normal y suponer que esos campos seguirán siendo ciertos. Si la configuración del cliente todavía puede cambiar `User`, `Port`, `ProxyJump`, `RemoteCommand`, los reenvíos o la selección de identidad, la aprobación cubrió una propuesta y no la acción ejecutada.

Un diseño más seguro congela la configuración efectiva material después de la lectura final. Como mínimo, vincula la aprobación al host efectivo, la dirección elegida, el puerto, el usuario remoto, la ruta del proxy, el comando remoto, las solicitudes de reenvío, el alias de clave de host y la fuente de identidad que puede usar el cliente. Si algún valor cambia antes de conectar, descarta la aprobación y vuelve a solicitarla. No conserves en silencio la aprobación durante una segunda evaluación.

## La procedencia de la configuración forma parte del destino

El destino final depende de los archivos de configuración, las opciones de línea de comandos, el entorno y la red local que haya evaluado OpenSSH. Aprobar un destino sin registrar esas entradas deja una vía sencilla para cambiar el significado autorizado. El manual de OpenSSH establece como orden normal las opciones de línea de comandos, el archivo de configuración del usuario y el archivo del sistema. Para la mayoría de las directivas se utiliza el primer valor obtenido.

Un agente capaz de añadir opciones SSH arbitrarias puede eludir un valor predeterminado cuidadoso. `-F` selecciona otro archivo de configuración, mientras que `-o` puede definir directamente `Hostname`, `ProxyCommand`, `ProxyJump`, `Port`, `User`, `HostKeyAlias` o las directivas de reenvío. Una pasarela debe analizar una acción SSH en campos tipados y rechazar opciones no admitidas en lugar de aceptar una cadena opaca. Si necesita admitir opciones sin procesar, debe clasificar todas las que afecten al destino e incluir su valor normalizado en la aprobación.

Los archivos de configuración aportan entradas transitivas. `Include` admite varias rutas, comodines, tokens y variables de entorno, y procesa las coincidencias de comodines en orden léxico. Añadir un archivo a un directorio incluido puede cambiar qué valor aparece primero sin tocar el archivo principal. Registra el resumen de contenido y el propietario de cada archivo cargado, no solo la ruta del primero. Rechaza archivos que pueda modificar el agente o un directorio de trabajo perteneciente a un repositorio que no sea de confianza.

`Match exec` no se limita a comparar una cadena condicional: OpenSSH ejecuta el comando indicado con el shell del usuario y considera que hay coincidencia si devuelve cero. `Match localnetwork` puede variar la configuración según las direcciones de las interfaces locales activas. El propio manual advierte que la red observada localmente no es un criterio fiable para configuración sensible cuando DHCP la configura. Esos predicados permiten que la misma solicitud `ssh build` produzca otro destino efectivo cuando se conecta una VPN, cambia el resultado de un script o aparece un archivo incluido.

El ejecutor de aprobaciones debe establecer un conjunto cerrado de entradas antes de evaluar. Usa un binario de cliente fijo, una raíz de configuración controlada, un entorno depurado, propietarios de archivo conocidos, una familia de direcciones explícita y una política declarada para la configuración de usuario y del sistema. Calcula un resumen de ese conjunto y colócalo en el ticket de ejecución. No tiene que dominar la tarjeta, pero debe figurar en la auditoría y volver a comprobarse al utilizarlo.

Congelar la configuración no exige prohibir ajustes útiles por host. Exige decidir quién puede escribirlos. Un alias gestionado por el operador que asocia `build` con un host canónico de producción puede ser seguro si la aprobación muestra esa asociación. Un alias creado dentro de un repositorio controlado por el agente forma parte de la solicitud no fiable, aunque su sintaxis parezca una configuración normal de OpenSSH.

Esta comprobación de procedencia también cierra una carencia habitual en las pruebas. Los equipos suelen probar la canonicalización con un archivo temporal limpio y después ejecutar con la configuración completa del desarrollador y los valores predeterminados del sistema. La prueba demuestra el archivo temporal, no la acción real. Captura una sola vez las entradas exactas evaluadas y consérvalas durante la aprobación y la conexión.

Trata también la versión de OpenSSH como una entrada. Los valores predeterminados y las directivas aceptadas cambian, y una flota puede contener distintas compilaciones aunque todas las máquinas lean el mismo archivo compartido. Registra en el ticket la ruta y la versión del ejecutable, y repite las pruebas de compatibilidad antes de que una actualización alcance el tráfico de agentes. Un analizador escrito para una versión debe rechazar una salida desconocida, no adivinar que un campo nuevo o renombrado es inocuo.

La procedencia también decide si se puede reutilizar una aprobación. Solo es defendible cuando la solicitud, toda la configuración evaluada, el resultado de resolución y el estado del conector permanecen idénticos durante un periodo breve. Un apodo coincidente no demuestra nada de eso. Es preferible una nueva aprobación concisa a un permiso duradero cuyo destino efectivo pueda variar entre ejecuciones del agente sin que nadie lo note.

## Las respuestas DNS pueden cambiar después de mostrar la tarjeta

Aunque el nombre efectivo no cambie, su dirección puede variar entre la aprobación y la conexión. La rotación DNS, las vistas divididas, los cambios de VPN, las rutas de búsqueda del resolvedor y las actualizaciones normales de registros pueden producir otra respuesta. Aparece una diferencia clásica entre comprobación y uso si el servicio resuelve el nombre, muestra una dirección y el proceso SSH vuelve a resolverla más tarde.

La solución no consiste en declarar que DNS no es fiable y omitir la dirección. Consiste en vincular la ejecución al resultado exacto que vio quien aprobó. Resuelve una vez dentro del ejecutor de confianza, selecciona una dirección según las reglas de familia del cliente, muéstrala y pasa esa dirección de socket a la conexión sin otra consulta de nombre. Conserva el nombre canónico para usos de identidad similares a la indicación de nombre de servidor cuando proceda, la búsqueda de claves, los certificados y el registro, pero no permitas que el transporte elija en silencio otra dirección.

Varias respuestas A o AAAA exigen una regla explícita. Mostrar un conjunto de direcciones y permitir al cliente probar cualquier miembro puede ser razonable, pero la aprobación debe decir que cubre el conjunto mostrado y la auditoría debe registrar qué miembro funcionó. Si el conjunto cambia, la aprobación antigua no debe ampliarse para incluir la dirección nueva. En hosts de alto riesgo, aprobar una sola dirección seleccionada resulta más fácil de razonar.

Un proxy cambia el lugar donde ocurre la resolución DNS local. Con `ProxyJump`, el cliente suele pedir que la conexión de salto lleve el tráfico hasta el host y puerto finales. `ProxyCommand` puede implementar casi cualquier comportamiento de transporte. La dirección visible localmente puede pertenecer al proxy mientras el nombre final se resuelve en otra parte. La aprobación necesita ambos saltos: el extremo concreto del proxy que alcanza el proceso local y el valor del host final enviado a través de él. Fingir que la dirección del proxy es el destino pierde el objetivo; fingir que no importa pierde la ruta de red.

## Una clave de host válida no valida el nombre solicitado

La comprobación estricta de claves de host protege un límite distinto al de la aprobación del destino. Debe seguir activa, pero no puede determinar si el resultado de la canonicalización estaba dentro de la intención humana. Las claves compartidas, los certificados con varios principales y un `HostKeyAlias` explícito pueden producir una conexión criptográficamente válida bajo un nombre distinto al mostrado.

La arquitectura de SSH en RFC 4251 describe una base local que relaciona el nombre de host escrito por el usuario con una clave. OpenSSH añade nombres gobernados por configuración sobre ese modelo básico. `HostKeyAlias` indica expresamente al cliente que use un alias en lugar del nombre real al buscar o guardar claves y al validar certificados. Es útil para túneles y varios servidores en una dirección, pero crea otro nombre que debe aparecer en la auditoría.

RFC 4255, que define los registros SSHFP, resulta muy pertinente. Advierte sobre nombres sin cualificar y rutas de búsqueda DNS inyectadas, y recomienda comprobar primero una base local de claves antes que una huella DNS en esa situación. También afirma que un registro SSHFP no debe considerarse fiable si DNSSEC no lo autenticó. El consejo se refiere a autenticar un servidor, pero refuerza una idea más amplia: ampliar un nombre cambia el contexto de seguridad y la salida del resolvedor por sí sola no prueba identidad.

RFC 4462 formula una advertencia más dura para GSS-API. Dice que una implementación no debe usar resultados DNS inseguros para construir el nombre objetivo del servidor, porque un atacante podría cambiar la asociación de un alias o una dirección y suplantarlo. Aunque el agente use autenticación ordinaria por clave pública, la lección de diseño se mantiene. Una reescritura de nombre sin protección no debe decidir en silencio la identidad que un control de seguridad afirma aprobar.

La verificación de la clave debe volver a vincular el resultado con el registro previo. Guarda la huella presentada, el nombre o alias usado en la búsqueda, si coincidió un principal del certificado y el resultado de la comprobación estricta. Si el cliente recurre a una pregunta para aceptar una clave nueva, se trata de otra decisión de seguridad. Un proceso autónomo no debe convertirla en aceptación automática solo porque alguien aprobó antes el comando de shell.

## La comprobación previa debe usar las mismas entradas de OpenSSH

Una comprobación útil reproduce la configuración final del cliente y observa la ruta sin ejecutar el comando remoto. `ssh -G` imprime la configuración tras evaluar los bloques `Host` y `Match`. `ssh -vvv` añade diagnósticos de resolución, conexión y clave de host; el manual define tres opciones `-v` como el máximo nivel de detalle.

Ejecuta esta secuencia en un entorno controlado y sustituye `build` por el token solicitado exacto:

```sh
ssh -G build | awk '
  $1 == "hostname" || $1 == "user" || $1 == "port" ||
  $1 == "proxyjump" || $1 == "proxycommand" ||
  $1 == "hostkeyalias" || $1 == "canonicalizehostname" {
    print
  }
'
ssh -vvv -o BatchMode=yes -o SessionType=none \
  -o ConnectTimeout=5 build 2>&1
```

El primer comando produce una salida con esta forma:

```text
user deploy
hostname build-07.hosts.example
port 22
canonicalizehostname true
proxyjump none
```

La ejecución detallada emite después líneas cuya redacción varía por versión, pero identifica el host ampliado, la dirección y el puerto elegidos, las pasadas de configuración y la clave ofrecida durante el intercambio. Captura datos estructurados desde el ejecutor en vez de tratar el texto de depuración como una API estable. Los comandos son excelentes para investigar; el código de producción debe obtener los mismos datos de la implementación que abre el socket.

Este diagnóstico tiene dos trampas. Primero, `ssh -G` muestra la configuración evaluada, pero no prueba qué dirección elegirá y alcanzará una conexión posterior. Segundo, la prueba detallada puede abrir una conexión y realizar el intercambio incluso con `SessionType=none`; ejecútala solo si la propia prueba está autorizada. `BatchMode=yes` elimina las preguntas interactivas de contraseña y clave, pero no convierte un intento de conexión en cálculo local.

Para que la investigación sea reproducible, aísla todas las entradas. Proporciona la configuración de usuario prevista con `-F`, contempla cómo trata la compilación la configuración del sistema, fija las variables usadas por `Include` o la expansión de tokens y registra la versión. Registra también si un socket de control podría reutilizar una conexión multiplexada. Una comprobación con DNS y configuración nuevos sirve de poco si la ejecución se conecta a una sesión maestra anterior.

No lances un proceso para aprobar y otro para ejecutar con análisis independientes. La arquitectura preferible usa un solo ejecutor que carga la configuración, canonicaliza, resuelve, se detiene con un registro candidato inmutable y continúa la misma máquina de estados tras la aprobación. Si la biblioteca no puede detenerse con seguridad, crea un ticket firmado o autenticado con todos los campos materiales y obliga al conector a rechazar cualquier diferencia.

## El registro de aprobación debe mostrar la transformación

Una buena tarjeta hace visible el cambio sin obligar a leer un registro de depuración. Coloca el destino solicitado junto al efectivo y su dirección. Si no cambió nada, indícalo con brevedad. Si la canonicalización o un bloque `Match` cambió un campo material, marca los valores anterior y nuevo.

Un registro práctico puede tener esta forma:

```json
{
  "requested": {
    "host": "build",
    "user": "deploy",
    "command": "/usr/local/bin/release status"
  },
  "effective": {
    "host": "build-07.hosts.example",
    "address": "192.0.2.44",
    "port": 22,
    "user": "deploy",
    "proxy": null,
    "host_key_alias": null
  },
  "resolution": {
    "canonicalized": true,
    "source": "build.ops.example",
    "permitted_cname": true
  },
  "binding": "sha256:REDACTED"
}
```

El vínculo debe cubrir una codificación determinista de los campos solicitados y efectivos, el comando remoto, los reenvíos, la identidad de configuración pertinente, la dirección o conjunto aprobado y una caducidad breve. El conector vuelve a calcularlo justo antes de abrir el socket. Si DNS, configuración, comando, usuario, puerto o proxy son distintos, se detiene.

No omitas un campo de la tarjeta solo porque esté incluido en el vínculo. Las personas autorizan lo que pueden ver. Muestra `build -> build-07.hosts.example (192.0.2.44)` como una transformación legible y, después, usuario, puerto, proxy y comando. Reserva las huellas y la procedencia para un detalle ampliable, salvo que una identidad nueva o modificada exija atención.

El caso incómodo es un nombre que resuelve a un grupo grande o volátil. No apruebes un concepto sin límites como cualquier dirección actual de este nombre. Elige un candidato, aprueba un conjunto publicado y limitado con caducidad, o exige una identidad estable más fuerte, como un principal de certificado respaldado por una CA de confianza. La aprobación debe declarar el modelo, no dejar que el conector lo deduzca.

## Los proxies y la reutilización necesitan vínculos separados

La canonicalización se comporta de otro modo con proxies. `CanonicalizeHostname yes` se aplica solo a conexiones sin `ProxyCommand` o `ProxyJump`; `always` incluye las conexiones con proxy. Una sola palabra puede cambiar el nombre final y activar la segunda lectura de configuración. El sistema de aprobación debe registrar el valor real y no suponer que la canonicalización está activa o inactiva en todas partes.

Cada host de salto es una conexión SSH con su propio nombre solicitado, nombre efectivo, dirección, usuario, puerto y clave autenticada. El destino final también mantiene su identidad a través del túnel. Aprobar solo la etiqueta final omite una ruta de salto inesperada o comprometida. Aprobar solo el servidor de salto omite adónde reenvía el flujo.

La multiplexación crea una omisión menos evidente. Si ya existe una sesión `ControlMaster` coincidente, otra invocación puede reutilizarla en vez de crear la conexión prevista. El maestro existente quizá resolvió DNS antes, usó una configuración anterior y autenticó una clave antes de la aprobación actual. Deshabilita la reutilización para acciones controladas o vincula la aprobación a la identidad inmutable de la sesión maestra y compruébala antes de abrir un canal.

La misma regla se aplica a los reintentos. Si falla una dirección, la segunda solo sigue autorizada si estaba en el conjunto mostrado. Un proxy alternativo, otro puerto o un nombre recién canonicalizado no son detalles de transporte. Cambian el objeto de autorización y exigen otra decisión.

## La auditoría debe conservar intención y resultado

Una entrada útil permite reconstruir toda la transformación sin volver a consultar DNS. Guarda la solicitud original, los campos efectivos, el nombre canónico, la cadena DNS, las direcciones candidatas, la seleccionada, los saltos, la huella aceptada, el principal del certificado si existe y el comando o subsistema. Marca por separado la hora de resolución y la de conexión para que la diferencia temporal sea visible.

Sallyport conduce las acciones SSH mediante su auxiliar `sp-ssh` mientras las claves permanecen en la bóveda cifrada, por lo que el agente nunca recibe la clave privada. Sus diarios Activity y Sessions proceden de un único registro cifrado y encadenado por hashes, que permite a la ruta de aprobación y ejecución conservar tanto el destino solicitado como el observado, no solo una cadena con aspecto de comando.

Conservar datos no basta. Define invariantes comprobables: la dirección ejecutada pertenecía al conjunto aprobado, el host y el puerto coincidían con el ticket, la identidad aceptada quedó registrada y todos los reintentos y saltos tenían autorización. Alimenta la prueba con `CanonicalizeHostname`, un CNAME permitido, un bloque `Match canonical` y dos respuestas DNS. Cambia un dato entre la comprobación y la conexión y confirma que la ejecución se detiene.

El fallo que interesa evitar es corriente: un agente pide `build`, una persona reconoce el apodo y aprueba, y un cliente en otra red lo amplía a otro dominio. La sesión puede seguir cifrada y el servidor puede presentar una clave válida para su propio nombre. La aprobación sigue siendo incorrecta. Coloca el nombre solicitado, el final y la dirección elegida en la misma decisión, y obliga al conector a demostrar que usó exactamente esos valores.
