This page looks best with JavaScript enabled

Centralized authorized keys through Redis server

 ·  🎃 kr0m

OpenSSH allows us to obtain the list of pubkeys from authorized_keys by running a command, so it will be possible to centralize the authorized keys in a single place on our servers. In this article, we will use a Redis server, but we must have a backup system in case Redis fails, in my case an HTTP request.

To perform the tests, I have created two Jails, one that will act as a Redis server and another as an Ssh server that will consult Redis to grant or deny Ssh access.

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         

We install Redis:

pkg install redis

We configure the binding IP and access password:

vi /usr/local/etc/redis.conf

bind 192.168.69.78
requirepass PASSWORD

We enable the Redis service and start it:

sysrc redis_enable="yes"
service redis start

Someone might think that a firewall is not necessary because we are only going to store pubkeys, but exposing these pubkeys can lead to other types of problems .

If the Jail uses VNET technology or it is a physical server, we will configure IPFW as explained in this article .

In my case, it is a configuration through an alias, so we run the following commands in the parent:

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

Let’s close all traffic allowing only what is strictly necessary:

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

Start the service:

service ipfw start

Check the rules:

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

When configuring a rule that uses the “me” keyword, it refers to any alias configured in the parent, i.e., the parent and any Jail. It should also be noted that we DO NOT allow traffic on the loopback interface since communication between IPs configured as aliases is done through the loopback. Allowing all traffic between Jails would be a security risk. Another interesting aspect is that when the parent connects to any of the alias IPs, the source IP is the alias IP itself (rules: 00413-00416).

An interesting feature of this system is that the same Redis can be used by several groups of servers by selecting a different DB. If we were to reach the limit of databases allowed by Redis, we could deploy different login scripts to query keys with a prefix. This way, we could use a single database for different groups of servers, for example:

webservers_root  
mailservers_root  
...

We insert the pubkeys for root, for which we will use Redis lists:

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’

On the Ssh server, we install the dependencies of our Python script, which will be executed by the Ssh service to determine whether to grant or deny access:

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'))

We set the owner, group, and permissions of the script:

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

We test the query manually:

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

Now we need to modify the Ssh service configuration to execute our script with the nobody user. We will modify the following parameters:

  • AuthorizedKeysCommandUser: System user to execute the AuthorizedKeysCommand script
  • AuthorizedKeysCommand: Command to execute to obtain the 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

We have enabled Ssh access for the root user to test without having to create an additional user in the system and access via Pubkey.

The AuthorizedKeysCommand parameter accepts the following 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.

We enable the Ssh service and start it:

sysrc sshd_enable="yes"
service sshd start

We check that we can log in via Ssh, but first we leave a monitor on the Redis server to check the execution of the query:

redis-cli -a PASSWORD monitor

We access the server via Ssh:

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

In the Redis monitor, a line like this will have appeared:

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

If we remove the key from the list, we can check that we have lost access to the server:

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”

We check that the list does not exist:

LRANGE root 0 -1

(empty list or set)

We check the loss of access to the server:

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

The traditional authorized_keys still works, therefore we must change the AuthorizedKeysFile parameter so that it checks a file that only root can modify. This parameter allows the following 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

We create the directory and restart the service:

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

If the connection to redis fails, the content of the authorized_keys from the URL indicated in the script would be downloaded. Downloading the file via HTTP is just an idea, as many fallbacks as desired can be implemented using the technology that suits us best.

If you liked the article, you can treat me to a RedBull here