En este post, veremos cómo generar un bastion host compuesto por un servidor SSH y un servicio de Fail2ban, para poder acceder a la infraestructura de una manera segura mediante certificados firmados por una CA de Vault con nuestro usuario de Keycloak.
Un bastion host se utiliza para centralizar las conexiones SSH hacia la infraestructura. Siendo así obligatorio pasar por este, minimizando los puntos de acceso a ella y por lo tanto reduciendo riesgos. Esto es una práctica muy habitual a la hora de securizar entornos. Esto unido al servicio de Fail2ban, nos permite bloquear atacantes de manera automática en el caso de que realicen un número concreto de conexiones denegadas en un periodo de tiempo.
Este sería el diagrama de la infraestructura que utilizaremos. Como se puede ver, hay varios servicios que intervienen. Pero para este post, nos vamos a centrar solo en la parte de SSH Bastion
. Para el resto, podéis consultar los siguientes posts;
Workflow
El flujo que se hará para realizar la conexión SSH, es el siguiente:
- Autenticación contra Vault (si no lo estamos ya) utilizando el protocolo OIDC.
- En el caso de que no lo estemos, esto nos redirigirá a Keycloak.
- Validaremos con nuestro usuario la sesión.
- Solicitaremos el certificado firmado con la CA de Vault (que la parte pública de esta, estará en todos los hosts destino a los que nos queramos conectar).
- Vault nos devolverá el certificado firmado válido para X tiempo.
- Podremos conectarnos a través del Bastion host a las máquinas de la infra.
Ventajas
- Centralizamos los accesos SSH desde un punto único.
- Es mas fácil monitorizar quien accede a la infraestructura, tanto a nivel de métricas como de logs
- No son necesarios servcios instalados en los hosts que autentiquen usuarios contra IDPs. Esto facilita mucho el trabajo ya que en infraestructuras dinámicas, no dependes de conexiones con otros servicios. Solo necesita la CA pública.
- Los accesos son para X tiempo, por lo que si se le revoca el acceso a alguien, solo podrá realizar conexiones durante el tiempo establecido (a diferencia de si se usan claves públicas-privadas)
- Al realizar la autenticación mediante SSO, no hay que manejar usuarios y contraseñas (a menos que se pierda la sesión)
Despliegue
La forma de desplegar esto, va a ser mediante Docker-compose. Desplegaremos un Docker con un servidor SSH y otro con un servicio de Fail2ban. La idea es que solo con levantar esto (si se tiene un Vault y un Keycloak ya pre-configurados) sea lo mas automático y transparente posible.
Vault
Hemos dicho que no vamos a desplegar un servidor de Vault porque esto ya está explicado. Pero si que vamos a explicar los pasos necesarios para poder utilizar este servicio con la autenticación SSH. En este post de Vault, lo explican todo.
Para empezar, necesitamos habilitar el secret engine
de SSH en Vault. En este caso, vamos a llamar al path: bastion
. Este puede variar si se tienen diferentes entornos por ejemplo y así no tener la misma CA y permisos para todos los entornos.
Nota, para instalar la CLI de vault, se puede ver desde aquí.
vault secrets enable -path=bastion ssh
A continuación, tenemos que generar la CA privada con la que firmaremos los certificados:
vault write bastion/config/ca generate_signing_key=true
Ahora, vamos a crear el role donde definiremos las opciones de los certificados. Aquí se puede definir el usuario, el tiempo antes de que expire, las extensiones…
vault write bastion/roles/bastion -<<"EOH"
{
"allow_user_certificates": true,
"allowed_users": "bastion",
"default_extensions": [
{
"permit-pty": "",
"permit-port-forwarding": ""
}
],
"key_type": "ca",
"default_user": "bastion",
"ttl": "30m0s",
"allow_user_key_ids": "false",
"key_id_format": "{{token_display_name}}"
}
EOH
Una vez creado el role, tenemos que crear las políticas que vamos a necesitar para poder firmar los certificados:
vault policy write bastion-ssh -<<EOF
path "bastion/sign/bastion" {
capabilities = ["create", "update"]
}
path "bastion/config/ca" {
capabilities = ["read"]
}
EOF
Y por último, vamos a modificar el role de autenticación contra Keycloak para que en vez de pasarnos el sub
, nos pase el preferred_username
y añadir la policy que hemos creado. De este modo, es mas fácil identificar luego en los logs que usuario está accediendo. Esto puede variar dependiendo de como se haya creado previamente a la hora de conectarlo con Keycloak.
vault write auth/keycloak/role/manager \
bound_audiences="vault" \
allowed_redirect_uris="https://vault.example.com/ui/vault/auth/keycloak/oidc/callback,https://vault.example.com/keycloak/callback,http://localhost:8250/oidc/callback" \
user_claim="preferred_username" \
policies="manager,bastion-ssh" \
ttl=1h \
role_type="oidc" \
oidc_scopes="openid"
Una vez que hayamos hecho esto, tenemos que asegurarnos de que las CAs públicas de Vault, están desplegadas en los hosts destino a los que nos vamos a conectar. Para esto solo hay que descargarlas mediante un curl
y permitirlas en sshd_config
.
curl -s -o /etc/ssh/vault.pem https://vault.example.com/v1/bastion/public_key
echo -e "\nTrustedUserCAKeys /etc/ssh/vault.pem" >> /etc/ssh/sshd_config
systemctl restart sshd
Bastion host
Para desplegar el bastion host, hay que hacer un clone del siguiente repo y levantarlo con Docker-compose. En este repo, está toda la configuración tanto del servidor SSH como del de Fail2ban. Los PR son bienvenidos para cualquier mejora 😉
git clone https://github.com/ichasco/BastionHost.git
Antes de levantarlo, hay que setear las variables necesarias en el fichero .env
## SSH
SSH_PORT=222
SSH_USER=bastion
SSH_CLIENT_ALIVE_INTERVAL=300
SSH_CLIENT_ALIVE_COUNT_MAX=2
SSH_MAX_AUTH_TRIES=3
SSH_RECORD_SESSION=false
### ENABLE VAULT INTEGRATION
VAULT_ENABLE=true
VAULT_HOST=vault.example.com
VAULT_SSH_PATH=bastion
## FAIL2BAN
FAIL2BAN_LOGLEVEL=INFO
FAIL2BAN_SSH_MAXRETRY=3
FAIL2BAN_SSH_BANTIME=86400
Por defecto, el contenedor escuchará en el puerto 222
y generará un usuario llamado bastion
.
Una vez establecidas las variables, solo queda levantar el stack. Para la primera vez, hay que lanzarlo con el --build
. El tiempo de creación del servidor SSH puede llevar mas tiempo la primera vez porque genera las claves nuevas. Esto es un añadido de seguridad.
docker-compose --compatibility up -d --build
Conexión SSH
Cuando termine de levantarse todo, podremos empezar a conectarnos a los hosts. Para facilitar esto, es recomendable añadir en el fichero ~/.ssh/config
la configuración de acceso al bastión. De este modo, solo habrá que invocarlo con el nombre que le demos. En este caso bastion_host
Host bastion_host
Hostname bastion.example.com
Port 222
User bastion
IdentityFile ~/.ssh/id_rsa.pub
CertificateFile ~/.ssh/signed-cert.pub
ProxyCommand none
PasswordAuthentication no
ForwardAgent no
Para conectarnos a una máquina, hay que hacer lo siguiente:
Autenticarnos contra Vault. Esto nos abrirá una web para validar la sesión contra Keycloak.
vault login -method=oidc -path=keycloak -no-print
Solicitar el certificado firmado
vault write -field=signed_key bastion/sign/bastion public_key=@$HOME/.ssh/id_rsa.pub > $HOME/.ssh/signed-cert.pub
Podemos comprobar el certificado con el siguiente comando:
ssh-keygen -L -f ~/.ssh/signed-cert.pub
~/.ssh/signed-cert.pub:
Type: ssh-rsa-cert-v01@openssh.com user certificate
Public key: RSA-CERT SHA256:b2UUBIq2zq432p5DxYlF5kQeGgTADXMrooBCxbNePbKZ3w
Signing CA: RSA SHA256:CXc3mgXzh1Unf3238QEri6L+MvFbJZqwNafgmXO2EozZwM (using ssh-rsa)
Key ID: "keycloak-bastion"
Serial: 98651818604632422814
Valid: from 2020-06-28T20:08:23 to 2020-06-28T20:38:53
Principals:
bastion
Critical Options: (none)
Extensions:
permit-port-forwarding
permit-pty
Añadir la key privada al ssh-agent
ssh-add -k
Realizar la conexión a través del bastion hacia el host de dentro de la infra que queramos, en este caso será host1
. El nombre bastion_host
es el que hemos definido en ~/.ssh/config
. Y el usuario SSH será bastion
ssh -J bastion -i $HOME/.ssh/signed-cert.pub bastion@host1
Parece que hay que realizar muchos pasos, pero se puede hacer un script básico para automatizar el proceso.
#!/bin/bash
PUBLIC_KEY=$HOME/.ssh/id_rsa.pub
PRIVATE_KEY=$HOME/.ssh/id_rsa
SSH_CERTIFICATE=$HOME/.ssh/signed-cert.pub
VAULT_PATH=bastion
VAULT_ROLE=bastion
BASTION_HOST=$1
HOST=$2
SSH_PORT=${3:-22}
export VAULT_ADDR=https://vault.example.com
if ! vault token lookup > /dev/null 2>&1; then
vault login -method=oidc -path=keycloak -no-print
fi
vault write -field=signed_key ${VAULT_PATH}/sign/${VAULT_ROLE} public_key=@${PUBLIC_KEY} > ${SSH_CERTIFICATE}
ssh-add -k > /dev/null 2>&1
ssh -J ${BASTION_HOST} -i ${PRIVATE_KEY} -i ${SSH_CERTIFICATE} ${VAULT_ROLE}@${HOST} -p${SSH_PORT}
Y para conectarnos, sería:
./script bastion host1
Nota: En el caso de que no queramos añadir en el fichero ~/.ssh/config
la configuración del bastion, nos podemos conectar usando el siguiente comando:
ssh -o ProxyCommand="ssh -i $HOME/.ssh/signed-cert.pub -W %h:%p bastion@bastion.example.com -p222" -i $HOME/.ssh/signed-cert.pub bastion@host1
Y con esto ya tendríamos montado el sistema de acceso por bastion host.
Cualquier mejora siempre es bienvenida. 🙂
Fail2ban
Como bonus, añado unos comandos útiles de Fail2ban para gestionar el servicio
Status de los bloqueos de ssh
docker exec -it fail2ban fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 1
| |- Total failed: 2
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Bloquear una IP manualmente
docker exec -it fail2ban fail2ban-client set sshd banip <IP>
Desbloquear una IP manualmente
docker exec -it fail2ban fail2ban-client set sshd unbanip <IP>
Troubleshooting
Bastion host
Si da el siguiente error al realizar la conexión SSH
userauth_pubkey: certificate signature algorithm ssh-rsa: signature algorithm no
t supported [preauth]
Puede ser por que las versiones de Vault anteriores a la 1.4.1, no son compatibles con OpenSSH 8. Para solucionar esto, hay que añadir lo siguiente en: /etc/ssh/sshd_config
CASignatureAlgorithms ssh-rsa
Y reiniciar el servicio de ssh
Fail2ban
En sistemas Linux que implementen la nueva versión de iptables NFT, dará problemas con docker. Por lo que hay que establecer la versión legacy. Esto hay que hacerlo a nivel de host donde se despliegue el docker de Fail2ban
sudo update-alternatives --config iptables
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/sbin/iptables-nft 20 auto mode
1 /usr/sbin/iptables-legacy 10 manual mode
2 /usr/sbin/iptables-nft 20 manual mode
Press <enter> to keep the current choice[*], or type selection number: 1
systemctl restart docker
TODO
- Mejorar cliente SSH
- Generar un dashboard en Grafana con métricas y logs
- Crear una API de status para Fail2ban
- Guardar las sesiones de los saltos
- Mostrar el Ceritificate_ID en vez del username en los logs
Also published on Medium.