Hasta hace poco mi forma de gestionar conexiones SSH era bastante rudimentaria.
Para empezar, tenía la mala práctica de utilizar la misma llave SSH para prácticamente todos mis servidores.
Algo como:
ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100
Como tampoco quería memorizar las IPs, usuarios y comandos completos, mi solución fue crear aliases directamente en mi .bashrc:
alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
alias staging="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.200"
alias personal-server="ssh -i ~/.ssh/id_rsa root@203.0.113.10"
Así podía simplemente ejecutar:
production
Y técnicamente funcionaba.
Pero con el tiempo empecé a encontrar varios problemas con esta solución.
Mis dotfiles
Actualmente tengo mis dotfiles versionados con Git para poder mantener y reutilizar mi configuración entre diferentes máquinas.
Eso incluye mi .bashrc.
Y ahí apareció el problema. Al tener la definición de mis conexiones SSH dentro de los aliases, estas también pasarían a formar parte de mi repositorio de Git.
No quería publicar innecesariamente las direcciones IP de mis servidores. No porque fueran un secreto o porque ocultarlas fuera una medida de seguridad, sino simplemente para evitar tener información donde no debería estar.
Mi primera idea fue buscar una forma de separar esos aliases del .bashrc.
Básicamente quería tener un archivo local con mis conexiones SSH que pudiera mantener fuera de mi repositorio. Buscando cómo hacerlo terminé descubriendo que estaba intentando solucionar un problema que SSH ya tenía resuelto.
No necesitaba tener otro archivo de aliases en mi máquina, sino utilizar el archivo ~/.ssh/config que SSH ya tiene para este propósito.
Descubriendo ~/.ssh/config
SSH permite definir configuraciones específicas para nuestras conexiones directamente en el archivo:
~/.ssh/config
Por ejemplo, uno de mis aliases:
alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
podía convertirse en:
Host production
HostName 192.168.1.100
User ubuntu
IdentityFile ~/.ssh/production
Ahora puedo conectarme simplemente ejecutando:
ssh production
production funciona como un alias para esa conexión, pero a diferencia de mi alias de Bash, ahora es SSH quien conoce la configuración.
Cada propiedad tiene una función:
Hostes el nombre que voy a utilizar para identificar la conexión.HostNamees la dirección real del servidor, ya sea una IP o un dominio.Useres el usuario con el que quiero conectarme.IdentityFileindica qué llave privada quiero utilizar.
También podemos configurar otras opciones.
Por ejemplo, si nuestro servidor utiliza un puerto diferente al puerto 22:
Host production
HostName 192.168.1.100
User ubuntu
Port 2222
IdentityFile ~/.ssh/production
Pero seguimos conectándonos exactamente de la misma manera:
ssh production
Una llave diferente para cada servidor
Descubrir ~/.ssh/config también me hizo cambiar otra mala práctica que tenía: utilizar la misma llave SSH en diferentes servidores.
Antes podía tener algo así:
alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
alias staging="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.200"
alias personal-server="ssh -i ~/.ssh/id_rsa root@203.0.113.10"
Todos utilizaban id_rsa.
Si esa llave llegara a comprometerse, tendría que reemplazarla en todos los lugares donde la estaba utilizando.
En cambio, podemos generar una llave diferente para cada servidor.
Generando una llave con Ed25519
Para llaves nuevas, una opción moderna es Ed25519. Podemos generar una llave para nuestro servidor de producción utilizando:
ssh-keygen -t ed25519 -f ~/.ssh/production
Con -t ed25519 indicamos el tipo de llave que queremos generar y con -f ~/.ssh/production definimos dónde queremos guardarla.
Después de ejecutar el comando tendremos dos archivos:
~/.ssh/production
~/.ssh/production.pub
production contiene nuestra llave privada. Esta es la que posteriormente utilizaremos en IdentityFile y debemos mantener únicamente en nuestra máquina.
production.pub contiene nuestra llave pública, que es la que podemos agregar al servidor al que queremos conectarnos.
Generando una llave con RSA
También podemos utilizar RSA. Por ejemplo, podemos generar una llave RSA de 4096 bits con:
ssh-keygen -t rsa -b 4096 -f ~/.ssh/production
En este caso:
-t rsaindica que queremos generar una llave RSA.-b 4096define el tamaño de la llave en 4096 bits.-f ~/.ssh/productionindica dónde queremos guardarla.
Al igual que con Ed25519, obtendremos nuestra llave privada y pública:
~/.ssh/production
~/.ssh/production.pub
Para llaves nuevas, Ed25519 suele ser una buena opción por su tamaño reducido y buen nivel de seguridad. RSA sigue siendo útil cuando necesitamos compatibilidad con sistemas que no soportan Ed25519.
Independientemente del tipo de llave que utilicemos, podemos repetir el proceso para cada uno de nuestros servidores:
ssh-keygen -t ed25519 -f ~/.ssh/production
ssh-keygen -t ed25519 -f ~/.ssh/staging
ssh-keygen -t ed25519 -f ~/.ssh/personal-server
Y finalmente configurar cada una dentro de ~/.ssh/config:
Host production
HostName 192.168.1.100
User ubuntu
IdentityFile ~/.ssh/production
Host staging
HostName 192.168.1.200
User ubuntu
IdentityFile ~/.ssh/staging
Host personal-server
HostName 203.0.113.10
User root
IdentityFile ~/.ssh/personal-server
Esto limita el alcance si alguna de las llaves llega a comprometerse y también hace mucho más sencillo revocar o reemplazar el acceso a un servidor específico.
Y desde mi perspectiva, conectarme sigue siendo igual de sencillo:
ssh production
ssh staging
ssh personal-server
También podemos utilizar archivos .pem
IdentityFile no está limitado a las llaves que normalmente encontramos dentro de ~/.ssh, como id_rsa o id_ed25519. También podemos utilizar archivos .pem.
Un ejemplo bastante común es AWS EC2.
Cuando creamos un key pair para conectarnos a una instancia EC2, AWS nos permite descargar la llave privada como un archivo .pem.
Normalmente podríamos conectarnos a una instancia con un comando parecido a este:
ssh -i ~/.ssh/my-ec2-server.pem ubuntu@203.0.113.10
El usuario dependerá de la imagen que estemos utilizando. En este ejemplo estamos asumiendo una instancia basada en Ubuntu.
Con ~/.ssh/config podemos mover toda esa información a nuestra configuración:
Host my-ec2-server
HostName 203.0.113.10
User ubuntu
IdentityFile ~/.ssh/my-ec2-server.pem
Y ahora simplemente podemos ejecutar:
ssh my-ec2-server
Para SSH, la extensión .pem no es lo realmente importante. En este caso, el archivo sigue conteniendo nuestra llave privada y IdentityFile simplemente indica dónde encontrarla.
Podemos incluso mezclar diferentes tipos de archivos en nuestra configuración:
Host personal-server
HostName 192.168.1.100
User deploy
IdentityFile ~/.ssh/personal-server
Host aws-production
HostName 203.0.113.10
User ubuntu
IdentityFile ~/.ssh/aws-production.pem
Y conectarnos de la misma forma:
ssh personal-server
ssh aws-production
Un detalle importante con los archivos .pem que descargamos de AWS es asegurarnos de que tengan permisos suficientemente restrictivos:
chmod 400 ~/.ssh/aws-production.pem
De lo contrario, SSH puede rechazar nuestra llave privada por tener permisos demasiado abiertos.
No solamente funciona con ssh
Otra ventaja que no tenía con mis aliases de .bashrc es que esta configuración no solamente sirve cuando ejecuto directamente el comando ssh.
Mis aliases solamente existían dentro de mi shell. Pero si quería copiar un archivo utilizando scp, nuevamente tenía que escribir toda la conexión:
scp -i ~/.ssh/id_rsa ./backup.sql ubuntu@192.168.1.100:/tmp/backup.sql
Con ~/.ssh/config, scp puede utilizar el mismo host:
scp ./backup.sql production:/tmp/backup.sql
Lo mismo ocurre con rsync:
rsync -av ./dist/ production:/var/www/app/
Y con otras herramientas que utilizan SSH por debajo.
La configuración deja de ser simplemente un atajo para escribir menos en la terminal y pasa a convertirse en un lugar centralizado donde definir cómo conectarme a cada servidor.
Configuración compartida entre varios servidores
SSH también permite evitar repetir configuraciones cuando varios servidores comparten ciertas propiedades.
Por ejemplo, si production y staging utilizan el mismo usuario:
Host production staging
User ubuntu
Host production
HostName 192.168.1.100
IdentityFile ~/.ssh/production
Host staging
HostName 192.168.1.200
IdentityFile ~/.ssh/staging
También podemos utilizar wildcards:
Host *.company
User ubuntu
Host api.company
HostName 192.168.1.100
IdentityFile ~/.ssh/api
Host worker.company
HostName 192.168.1.200
IdentityFile ~/.ssh/worker
Y conectarnos normalmente:
ssh api.company
Esto puede ser especialmente útil cuando empezamos a manejar una mayor cantidad de servidores.
Estaba solucionando el problema en la capa equivocada
Lo curioso es que inicialmente no estaba buscando una mejor forma de utilizar SSH. Solamente quería sacar las IPs de mis servidores de mi .bashrc para evitar que terminaran publicadas junto con mis dotfiles.
Pero buscando una solución terminé descubriendo ~/.ssh/config. Y eso terminó solucionando varios problemas al mismo tiempo.
Pasé de tener:
alias production="ssh -i ~/.ssh/id_rsa ubuntu@192.168.1.100"
a simplemente:
ssh production
Mis conexiones dejaron de estar mezcladas con la configuración de mi shell, pude empezar a utilizar llaves diferentes para cada servidor y herramientas como
scporsyncpudieron reutilizar exactamente la misma configuración.
Al final, mis aliases de .bashrc eran básicamente una versión casera y mucho más limitada de algo que SSH ya tenía resuelto.
Y todo empezó porque no quería subir unas IPs a mi repositorio de dotfiles.