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.
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:
We configure the binding IP and access password:
bind 192.168.69.78
requirepass PASSWORD
We enable the Redis service and start it:
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_script="/etc/ipfw.rules"
sysrc firewall_logif=“YES”
Let’s close all traffic allowing only what is strictly necessary:
#!/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:
Check the rules:
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:
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:
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:
chmod 750 /usr/local/bin/userkeys.py
We test the query manually:
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
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:
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:
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:
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:
(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
AuthorizedKeysFile /usr/local/ssh/authorized_keys/%u
We create the directory and restart the service:
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.