Bastion host con certificados auto-firmados por Vault

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:

  1. Autenticación contra Vault (si no lo estamos ya) utilizando el protocolo OIDC.
  2. En el caso de que no lo estemos, esto nos redirigirá a Keycloak.
  3. Validaremos con nuestro usuario la sesión.
  4. 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).
  5. Vault nos devolverá el certificado firmado válido para X tiempo.
  6. 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.

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *