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.
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:
Configuramos la ip de bindeo y el password de acceso:
bind 192.168.69.78
requirepass PASSWORD
Habilitamos el servicio Redis y lo arrancamos:
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_script="/etc/ipfw.rules"
sysrc firewall_logif=“YES”
Vamos a cerrar todo el tráfico permitiendo solo lo estrictamente necesario:
#!/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:
Comprobamos las reglas:
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:
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:
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:
chmod 750 /usr/local/bin/userkeys.py
Probamos la consulta manualmente:
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
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:
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:
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:
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:
(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
AuthorizedKeysFile /usr/local/ssh/authorized_keys/%u
Creamos el directorio y reiniciamos el servicio:
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.