Esta pagina se ve mejor con JavaScript habilitado

Authorized keys centralizadas mediante servidor Redis

 ·  🎃 kr0m

OpenSSH nos permite obtener la lista de pubkeys del authorized_keys mediante la ejecución de un comando, de este modo será posible centralizar en un único lugar las keys autorizadas en nuestros servidores, en este artículo vamos a utilizar un servidor Redis pero debemos tener un sistema de backup por si Redis fallara, en mi caso una petición HTTP.

Para realizar las pruebas he creado dos Jails, una que actuará como servidor Redis y otra a modo de servidor Ssh que consultará el Redis para conceder o denegar el acceso Ssh.

cbsd jls

JNAME   JID  IP4_ADDR       HOST_HOSTNAME           PATH                     STATUS  
redis   2    192.168.69.78  redis.alfaexploit.com   /usr/jails/jails/redis   On           
test00  3    192.168.69.79  test00.alfaexploit.com  /usr/jails/jails/test00  On         

Instalamos Redis:

pkg install redis

Configuramos la ip de bindeo y el password de acceso:

vi /usr/local/etc/redis.conf

bind 192.168.69.78
requirepass PASSWORD

Habilitamos el servicio Redis y lo arrancamos:

sysrc redis_enable="yes"
service redis start

Alguien podría pensar que no es necesario un firewall porque solo vamos a almacenar pubkeys, pero exponer estas pubkeys puede acarrear otro tipo de problemas .

Si la Jail utiliza tecnología VNET o se trata de un servidor físico configuraremos IPFW tal como se explica en este artículo .

En mi caso se trata de una configuración mediante alias, así que ejecutamos los siguientes comandos en el padre:

sysrc firewall_enable="yes"
sysrc firewall_script="/etc/ipfw.rules"
sysrc firewall_logif=“YES”

Vamos a cerrar todo el tráfico permitiendo solo lo estrictamente necesario:

vim /etc/ipfw.rules

#!/bin/sh
# Flush out the list before we begin.
ipfw -q -f flush

# Set rules command prefix
cmd="ipfw -q add"
wanif="em0"

# No restrictions on Loopback Interface
#$cmd 00010 allow all from any to any via lo0

# Allow access to public DNS
# DNS TCP
$cmd 00110 allow tcp from me to 8.8.8.8 53 out via $wanif
$cmd 00110 allow tcp from 8.8.8.8 53 to me in via $wanif

$cmd 00110 allow tcp from me to 8.8.4.4 53 out via $wanif
$cmd 00110 allow tcp from 8.8.4.4 53 to me in via $wanif

# DNS UDP
$cmd 00111 allow udp from me to 8.8.8.8 53 out via $wanif
$cmd 00111 allow udp from 8.8.8.8 53 to me in via $wanif

$cmd 00111 allow udp from me to 8.8.4.4 53 out via $wanif
$cmd 00111 allow udp from 8.8.4.4 53 to me in via $wanif

# Allow outbound HTTP and HTTPS connections
$cmd 00200 allow tcp from me to any 80 out via $wanif
$cmd 00200 allow tcp from any 80 to me in via $wanif

$cmd 00220 allow tcp from me to any 443 out via $wanif
$cmd 00220 allow tcp from any 443 to me in via $wanif

# Allow outbound email connections
$cmd 00230 allow tcp from me to any 25 out via $wanif
$cmd 00230 allow tcp from any 25 to me in via $wanif

$cmd 00231 allow tcp from me to any 110 out via $wanif
$cmd 00231 allow tcp from any 110 to me in via $wanif

# Allow outbound ping
$cmd 00250 allow icmp from me to any out via $wanif
$cmd 00250 allow icmp from any to me in via $wanif

# Allow outbound NTP
$cmd 00260 allow udp from me to any 123 out via $wanif
$cmd 00260 allow udp from any 123 to me in via $wanif

# Allow outbound SSH
$cmd 00280 allow tcp from me to any 22 out via $wanif
$cmd 00280 allow tcp from any 22 to me in via $wanif

# Allow inbound SSH
$cmd 00311 allow tcp from any to me 22 in via $wanif
$cmd 00312 allow tcp from me 22 to any out via $wanif

# Allow redis traffic from test00 to redis server
$cmd 00411 allow tcp from 192.168.69.79 to 192.168.69.78 6379
$cmd 00412 allow tcp from 192.168.69.78 6379 to 192.168.69.79

# Allow redis traffic from parent host to redis server
$cmd 00413 allow tcp from 192.168.69.78 to 192.168.69.78 6379
$cmd 00414 allow tcp from 192.168.69.78 6379 to 192.168.69.78

# Allow ssh traffic from parent host to test00 server
$cmd 00415 allow tcp from 192.168.69.79 to 192.168.69.79 22
$cmd 00416 allow tcp from 192.168.69.79 22 to 192.168.69.79

# Deny and log all other connections
$cmd 00499 deny log all from any to any

Arrancamos el servicio:

service ipfw start

Comprobamos las reglas:

ipfw list

00110 allow tcp from me to 8.8.8.8 53 out via em0  
00110 allow tcp from 8.8.8.8 53 to me in via em0  
00110 allow tcp from me to 8.8.4.4 53 out via em0  
00110 allow tcp from 8.8.4.4 53 to me in via em0  
00111 allow udp from me to 8.8.8.8 53 out via em0  
00111 allow udp from 8.8.8.8 53 to me in via em0  
00111 allow udp from me to 8.8.4.4 53 out via em0  
00111 allow udp from 8.8.4.4 53 to me in via em0  
00200 allow tcp from me to any 80 out via em0  
00200 allow tcp from any 80 to me in via em0  
00220 allow tcp from me to any 443 out via em0  
00220 allow tcp from any 443 to me in via em0  
00230 allow tcp from me to any 25 out via em0  
00230 allow tcp from any 25 to me in via em0  
00231 allow tcp from me to any 110 out via em0  
00231 allow tcp from any 110 to me in via em0  
00250 allow icmp from me to any out via em0  
00250 allow icmp from any to me in via em0  
00260 allow udp from me to any 123 out via em0  
00260 allow udp from any 123 to me in via em0  
00280 allow tcp from me to any 22 out via em0  
00280 allow tcp from any 22 to me in via em0  
00311 allow tcp from any to me 22 in via em0  
00312 allow tcp from me 22 to any out via em0  
00411 allow tcp from 192.168.69.79 to 192.168.69.78 6379  
00412 allow tcp from 192.168.69.78 6379 to 192.168.69.79  
00413 allow tcp from 192.168.69.78 to 192.168.69.78 6379  
00414 allow tcp from 192.168.69.78 6379 to 192.168.69.78  
00415 allow tcp from 192.168.69.79 to 192.168.69.79 22  
00416 allow tcp from 192.168.69.79 22 to 192.168.69.79  
00499 deny log ip from any to any  
65535 deny ip from any to any

Cuando se configura una regla que emplea la palabra clave “me” se hace referencia a cualquier alias configurado en el padre, o sea el padre y cualquier Jail. También hay que recalcar que NO permitimos el tráfico en la interfaz loopback ya que la comunicación entre ips configuradas como alias se realiza mediante la loopback, estaríamos permitiendo todo el tráfico entre Jails, otro aspecto interesante es que cuando el padre conecta con alguna de las ips-alias la ip origen es la ip-alias en sí misma(reglas: 00413-00416).

Algo interesante de este sistema es que un mismo Redis puede ser utilizado por varios grupos de servidores seleccionando una DB distinta, si llegásemos al límite de bases de datos que permite Redis podríamos desplegar distintos script de login para consultar las keys con un prefijo, de este modo podríamos utilizar una única base de datos para distintos grupos de servidores, por ejemplo:

webservers_root  
mailservers_root  
...

Insertamos las pubkeys para root, para ello utilizaremos las listas de Redis:

redis-cli -a PASSWORD
LPUSH root ‘ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow’

En el servidor Ssh instalamos las dependencias de nuestro script en Python el cual será ejecutado por el servicio Ssh para determinar si conceder o denegar el acceso:

pkg install py37-pip
pip install redis
vi /usr/local/bin/userkeys.py

#!/usr/local/bin/python3.7

import redis
import sys
import urllib.request

#print("Number of arguments: %s"%(len(sys.argv)))
#print("The arguments are: %s"%(str(sys.argv)))

if len(sys.argv) != 2:
    #print('++ Invalid number of arguments')
    exit()

#print("First argument: %s"%(sys.argv[1]))

try:
    #print('-- Connecting to redis server')
    r = redis.Redis(host='192.168.69.78', port=6379, db=0, password='PASSWORD')
    authorized_keys = r.lrange(sys.argv[1], '0', '-1')

    for key in authorized_keys:
        print(key.decode("utf-8"))
except:
    #print('++ Cant connect to redis server')
    url = 'http://alfaexploit.com/uploads/authorized_keys/' + sys.argv[1]
    try:
        sysadmin_authorized_keys = urllib.request.urlopen(url)
    except:
        #print('++ Cant connect to http server')
        exit()
    print(sysadmin_authorized_keys.read().decode("utf-8").strip('\n'))

Seteamos el propietario, grupo y permisos del script:

chown root:nobody /usr/local/bin/userkeys.py
chmod 750 /usr/local/bin/userkeys.py

Probamos la consulta manualmente:

su -m nobody -c ‘/usr/local/bin/userkeys.py root’

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow

Ahora queda modificar la configuración del servicio Ssh para que ejecute nuestro script con el usuario nobody, para ello modificaremos los parámetros:

  • AuthorizedKeysCommandUser: Usuario del sistema con el que ejecutar el script AuthorizedKeysCommand
  • AuthorizedKeysCommand: Comando a ejecutar para obtener las authorized_keys
vi /etc/ssh/sshd_config
ListenAddress 192.168.69.79 
AuthorizedKeysCommand /usr/local/bin/userkeys.py %u
AuthorizedKeysCommandUser nobody
PermitRootLogin yes 
PubkeyAuthentication yes

Hemos habilitado el acceso Ssh para el usuario root para poder hacer la prueba sin tener que crear un usuario adicional en el sistema y el acceso mediante Pubkey.

El parámetro AuthorizedKeysCommand acepta los siguientes tokens:

%%  A literal `%'.
%f  The fingerprint of the key or certificate.
%h  The home directory of the user.
%k  The base64-encoded key or certificate for authentication.
%t  The key or certificate type.
%U  The numeric user ID of the target user.
%u  The username.

Habilitamos el servicio Ssh y lo arrancamos:

sysrc sshd_enable="yes"
service sshd start

Comprobamos que nos deje hacer login por Ssh, pero antes dejamos un monitor en el servidor Redis para comprobar la ejecución de la consulta:

redis-cli -a PASSWORD monitor

Accedemos por Ssh al servidor:

Last login: Sun Oct 11 01:11:56 2020 from 192.168.69.79  
FreeBSD 12.1-RELEASE-p10 GENERIC   
test00:/root@[1:21] #

En el monitor de Redis habrá aparecido una línea como esta:

1602372198.183623 [0 192.168.69.79:46083] "LRANGE" "root" "0" "-1"

Si eliminamos la key de la lista podremos comprobar que hemos perdido el acceso al servidor:

redis-cli -a PASSWORD
LREM root 0 “ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow”

Comprobamos que no exista la lista:

LRANGE root 0 -1

(empty list or set)

Comprobamos la perdida de acceso al servidor:

root@192.168.69.79: Permission denied (publickey,keyboard-interactive).

El authorized_keys tradicional sigue funcionando, por lo tanto debemos cambiar el parámetro AuthorizedKeysFile para que se compruebe un fichero que solo root pueda modificar, este parámetro permite los siguientes tokens:

%h: home directory of the user being authenticated
%u: login name of the user
vi /etc/ssh/sshd_config
AuthorizedKeysFile /usr/local/ssh/authorized_keys/%u

Creamos el directorio y reiniciamos el servicio:

mkdir -p /usr/local/ssh/authorized_keys/
service sshd restart

Si la conexión al redis fallara se bajaría el contenido del authorized_keys de la URL indicada en el script, la descarga del fichero por HTTP es solo una idea, se pueden implementar tantos fallbacks como deseemos utilizando la tecnología que mas nos convenga.

Si te ha gustado el artículo puedes invitarme a un RedBull aquí