Today we are going to solve a problem which has been given to me by my colleague. The wanted to build a infrastructure where every user can register through a portal and upload their public key. When the user tries to login to the system, OpenSSH server will execute a python script (planning to upgrade it to rust) and generate appropriate string so that that specific user can access his / her own LinuX Container (LXC).

server configuration

On the server we first need to install the openssh server. Run the command below in order to install the ssh server.

sudo apt install openssh-server

After installation, we change the following lines in the /etc/ssh/sshd_config file.

AuthorizedKeysCommand /srv/p2.py %k
AuthorizedKeysCommandUser root

Here we are instructing the openssh server execute command when checking for public keys. This feature is quite interesting as it paves way for a system administrator to obtain key from any other sources like LDAP and perform all sort of actions. In our scenario, we will use it to run a python script located in /srv/p2.py. After the command we can also see a parameter is being passed called %k. The purpose of this parameter can be obtained from openbsd [1] site.

Below we have also shown you the listing of the /srv/ location. Please note that permissions must be properly set in order for openssh to run the script.

root@pdojo1:/srv# ls -alh /srv/
total 4.7M
drwxr-xr-x  2 root root 4.0K Nov 28 10:53 .
drwxr-xr-x 20 root root 4.0K Nov 28 09:53 ..
-rw-r--r--  1 root root 1.2K Nov 28 10:54 list.csv
-rwxr-xr-x  1 root root  733 Nov 28 11:04 p2.py

in the python script file we have the following content. If we explain the following script, we can see that we take the parameter passed in by openssh server and take that into clientkey variable. After that we open the Comma Separated Value (CSV) file and search for the key. If the key is found, we will then use the corresponding LXC container name and assign it to lxdbox variable. After that the print function will replace the values and present the text as per provide in authorized_keys format.

The below print function basically follows the authorized_keysformat and uses one of its features called command. In the command parameter we assign the LXC box name and most importantly the user parameter because without it, LXC will enter the user into the container as root.

#!/usr/bin/env python3

import sys
import os
import csv

clientkey = sys.argv[1]
key=""
lxdbox = ""
with open('/srv/list.csv', newline='') as csvfile:
	reader = csv.DictReader(csvfile)
	for row in reader:
		if row['key'] == clientkey:
			key = row['key']
			lxdbox = row['lxdbox']

print(f'command="lxc exec {lxdbox}  --user 1001  -- bash" ssh-rsa {key}')

Below is the content from for two users which are called luser1 and luser2on the source server. This is not important but all other columns are critical.

ruser,key,lxdbox
luser1,AAA...[SNIP]...hcaT0=,box1
luser2,AAA...[SNIP]...giHk8=,box2

Now if we restart the openssh server, the python script should be executed. If the script is not working properly, it will prompt you for user name and password which means it is now working. At this phase you can also see some error message regarding the LXC container saying something like no container found, it means we are in the right track.

container configuration

As installing lxd container is out of the scope I will not go into details but just to let you know, I have used the following command to install.

apt install lxd-installer
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  lxd-installer
0 upgraded, 1 newly installed, 0 to remove and 4 not upgraded.
Need to get 3,108 B of archives.
After this operation, 22.5 kB of additional disk space will be used.
... [SNIP] ...
What should the new bridge be called? [default=lxdbr0]: 
What IPv4 address should be used? (CIDR subnet notation, \u201cauto\u201d or \u201cnone\u201d) [default=auto]: 
What IPv6 address should be used? (CIDR subnet notation, \u201cauto\u201d or \u201cnone\u201d) [default=auto]: 
Would you like the LXD server to be available over the network? (yes/no) [default=no]: 
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]: 
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: 
lxc launch ubuntu:jammy box1

Connect to the lxc container using command Then we configure by adding another user into the box

adduser hacker

Now lets copy the box to another one.

lxc copy box1 box2

Start the containers and view the list as below:

root@pdojo1:/srv# lxc list
+------------+---------+-----------------------+-----------------------------------------------+-----------+-----------+
|    NAME    |  STATE  |         IPV4          |                     IPV6                      |   TYPE    | SNAPSHOTS |
+------------+---------+-----------------------+-----------------------------------------------+-----------+-----------+
| box1       | RUNNING | 10.194.165.161 (eth0) | fd42:e0e6:5725:a233:216:3eff:fe58:ff0c (eth0) | CONTAINER | 0         |
+------------+---------+-----------------------+-----------------------------------------------+-----------+-----------+
| box2       | RUNNING | 10.194.165.93 (eth0)  | fd42:e0e6:5725:a233:216:3eff:fe64:4285 (eth0) | CONTAINER | 0         |
+------------+---------+-----------------------+-----------------------------------------------+-----------+-----------+
| lucky-wolf | STOPPED |                       |                                               | CONTAINER | 0         |
+------------+---------+-----------------------+-----------------------------------------------+-----------+-----------+

Now If we try to connect from luser1 and luser2. we will see that the connection is basically being routed toward appropriate containers. From below screenshot we can see that two users with the same username has been given access to separate LXD container boxes.

06b80ee362b9790a1a8f8c32b5443a9c.png

references

[1] https://man.openbsd.org/sshd_config