SSH connection directly to LXD container
Table of Contents
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_keys
format 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 luser2
on 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.