Skip to content

Commit e710163

Browse files
committed
Add Docker for SSH + 2FA + X11
1 parent a33c2e4 commit e710163

File tree

22 files changed

+316
-43
lines changed

22 files changed

+316
-43
lines changed

autosubmit/platforms/paramiko_platform.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,7 @@ def __init__(self, expid: str, name: str, config: dict, auth_password: Optional[
105105
self._wrapper = None
106106
self.remote_log_dir = ""
107107
# self.get_job_energy_cmd = ""
108-
display = os.getenv('DISPLAY', "localhost:0")
109-
try:
110-
self.local_x11_display = xlib_connect.get_display(display)
111-
except Exception as e:
112-
Log.warning(f"X11 display not found: {e}")
113-
self.local_x11_display = None
108+
self._init_local_x11_display()
114109

115110
@property
116111
def header(self):
@@ -313,14 +308,7 @@ def connect(
313308
:param log_recovery_process: Specifies if the call is made from the log retrieval process.
314309
"""
315310
try:
316-
display = os.getenv('DISPLAY')
317-
if display is None:
318-
display = "localhost:0"
319-
try:
320-
self.local_x11_display = xlib_connect.get_display(display)
321-
except Exception as e:
322-
Log.warning(f"X11 display not found: {e}")
323-
self.local_x11_display = None
311+
self._init_local_x11_display()
324312
self._ssh = _create_ssh_client()
325313
self._ssh_config = paramiko.SSHConfig()
326314
if as_conf:
@@ -1044,7 +1032,9 @@ def x11_status_checker(self, session, session_fileno):
10441032
counterpart.close()
10451033
del self.channels[fd]
10461034

1047-
def exec_command(self, command, bufsize=-1, timeout=30, get_pty=False, retries=3, x11=False):
1035+
def exec_command(
1036+
self, command, bufsize=-1, timeout=30, get_pty=False, retries=3, x11=False
1037+
) -> Union[tuple[paramiko.Channel, paramiko.Channel, paramiko.Channel], tuple[bool, bool, bool]]:
10481038
"""
10491039
Execute a command on the SSH server. A new `.Channel` is opened and
10501040
the requested command is execed. The command's input and output
@@ -1067,16 +1057,11 @@ def exec_command(self, command, bufsize=-1, timeout=30, get_pty=False, retries=3
10671057
while retries > 0:
10681058
try:
10691059
if x11:
1070-
display = os.getenv('DISPLAY')
1071-
if display is None or not display:
1072-
display = "localhost:0"
1073-
try:
1074-
self.local_x11_display = xlib_connect.get_display(display)
1075-
except Exception as e:
1076-
Log.warning(f"X11 display not found: {e}")
1077-
self.local_x11_display = None
1060+
self._init_local_x11_display()
10781061
chan = self.transport.open_session()
1079-
chan.request_x11(single_connection=False, handler=self.x11_handler)
1062+
if not chan.request_x11(single_connection=False, handler=self.x11_handler):
1063+
# FIXME: test this!
1064+
raise AutosubmitCritical(f"Remote platform does not support X11!")
10801065
else:
10811066
chan = self.transport.open_session()
10821067
if x11:
@@ -1085,12 +1070,11 @@ def exec_command(self, command, bufsize=-1, timeout=30, get_pty=False, retries=3
10851070
if timeout_command == 0:
10861071
timeout_command = "infinity"
10871072
command = f'{command} ; sleep {timeout_command} 2>/dev/null'
1088-
# command = f'export display {command}'
10891073
Log.info(command)
10901074
try:
10911075
chan.exec_command(command)
1092-
except BaseException as e:
1093-
raise AutosubmitCritical(f"Failed to execute command: {e}")
1076+
except Exception as e:
1077+
raise AutosubmitCritical(f"Failed to execute command '{command}': {e}")
10941078
chan_fileno = chan.fileno()
10951079
self.poller.register(chan_fileno, select.POLLIN)
10961080
self.x11_status_checker(chan, chan_fileno)
@@ -1552,6 +1536,21 @@ def read_file(self, src: str, max_size: int = None) -> Union[bytes, None]:
15521536
Log.debug(f"Error reading file {src}")
15531537
return None
15541538

1539+
def _init_local_x11_display(self) -> None:
1540+
"""Initialize the X11 display on this platform."""
1541+
display = os.getenv('DISPLAY', 'localhost:0')
1542+
try:
1543+
self.local_x11_display = xlib_connect.get_display(display)
1544+
except Exception as e:
1545+
Log.warning(f"X11 display not found: {e}")
1546+
self.local_x11_display = None
1547+
1548+
def _init_poller(self):
1549+
if sys.platform != "linux":
1550+
self.poller = select.kqueue()
1551+
else:
1552+
self.poller = select.poll()
1553+
15551554

15561555
class ParamikoPlatformException(Exception):
15571556
"""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM ghcr.io/linuxserver/openssh-server:latest
2+
3+
RUN \
4+
echo "**** install runtime packages ****" && \
5+
apk add --no-cache --upgrade \
6+
google-authenticator \
7+
xauth \
8+
xclock \
9+
xorg-server && \
10+
rm -rf \
11+
/tmp/* \
12+
$HOME/.cache
13+
14+
ADD rootfs/ /
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# linuxserverio-ssh-2fa-x11
2+
3+
This is a container based on the [SSH image of LinuxServer.io](https://docs.linuxserver.io/images/docker-openssh-server/).
4+
This image is already used in other tests of Autosubmit, and was tested on EDITO as well.
5+
6+
Here, we use that image as base image, and install and configure two-factor authentication
7+
with Google Authenticator. The image uses a test key for Google Authenticator, and a list
8+
of five backup codes. For testing, we only use the backup codes, as the container is
9+
destroyed after every test (i.e. you can connect up to five times, but normally a
10+
test will require just one connection).
11+
12+
The container also contains the required tools and configuration to support X11 forwarding,
13+
which is used in other tests where Autosubmit configures the Python Paramiko SSH Library
14+
to do X11 forwarding.
15+
16+
The idea of this container is a box to be used in tests to prevent regressions with
17+
users that use platforms with 2FA enabled, and users that rely on X11 for their
18+
workflows.
19+
20+
> NOTE: Use this only in integration tests.
21+
22+
The container with just SSH and Google Authenticator occupies 38.5 MB.
23+
When the X11 libraries are added (`xorg-server`, `xauth`, and `xclock` for testing)
24+
it grows to 395 MB. So be aware of that when running these locally.
25+
26+
> TODO: Maybe we can find a smaller or test X11 server?
27+
28+
## Building
29+
30+
To build the container (note the `--load`, needed when using `buildx`, remove it if not):
31+
32+
```bash
33+
$ docker build --load . -t autosubmit/linuxserverio-ssh-2fa-x11:latest
34+
```
35+
36+
## Running
37+
38+
By default, the container will listen on port 22, it will have X11 installed
39+
and configured, but 2FA will not be enabled.
40+
41+
To run the container, you can use the same arguments as the LinuxServer.io
42+
image, plus the `MFA=<bool>` flag to control whether 2FA is enabled or not:
43+
44+
```bash
45+
$ docker run \
46+
--rm \
47+
--name ssh \
48+
-p1234:22 \
49+
--env MFA=false \
50+
--env TZ=Etc/UTC \
51+
--env SUDO_ACCESS=false \
52+
--env USER_NAME=as_user \
53+
--env USER_PASSWORD=password \
54+
--env PUID=1000 \
55+
--env PGID=1000 \
56+
--env UMASK=000 \
57+
--env PASSWORD_ACCESS=true \
58+
autosubmit/linuxserverio-ssh-2fa-x11:latest
59+
```
60+
61+
To connect via SSH, you will need to run the following (note: this can be
62+
automated in a Pytest fixture!):
63+
64+
```bash
65+
$ docker exec -ti ssh /bin/bash
66+
root@c55bcd13ae8f:/# mkdir /root/.ssh
67+
root@c55bcd13ae8f:/# echo "ssh-rsa AAAAB3...." > /root/.ssh/authorized_keys
68+
root@c55bcd13ae8f:/#
69+
exit
70+
$ ssh -X root@localhost -p 1234
71+
```
72+
73+
At this point, you should be connected, and you should be able to run the
74+
`xclock` program in the SSH server to verify that X11 forwarding is working.
75+
76+
To run it with 2FA, you must specify the environment variable `MFA=true`
77+
when running the container:
78+
79+
```bash
80+
$ docker run \
81+
... \
82+
--env MFA=true \
83+
... \
84+
autosubmit/linuxserverio-ssh-2fa-x11:latest
85+
```
86+
87+
Repeat the previous steps for the SSH key, then try to connect via SSH.
88+
89+
When challenged for the SSH 2FA code, you can use the first of the list of
90+
backup codes, `55192054`. Remember that that code is a one-time use.
91+
92+
The current secret for Google Authenticator is `X5C4VLHDCECGFHPP3PHDCS7KAE`,
93+
and the backup codes available are:
94+
95+
- 55192054
96+
- 18998816
97+
- 51868999
98+
- 99315827
99+
- 42932878
100+
101+
If you rebuild the image, just `docker login` and `docker push`, taking
102+
care to use the correct image, and testing it as well.
103+
104+
Happy testing!

docker/ssh/linuxserverio-ssh-with-2fa-x11/rootfs/etc/s6-overlay/s6-rc.d/init-openssh-mfa-config/dependencies.d/init-openssh-server-config

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/with-contenv bash
2+
# shellcheck shell=bash
3+
4+
USER_NAME=${USER_NAME:-linuxserver.io}
5+
HOME_DIR=$(getent passwd "${USER_NAME}" | cut -d: -f6)
6+
7+
if [[ "$MFA" == "true" ]]; then
8+
echo "Enabling SSH MFA"
9+
# sshd
10+
sed -i 's/#UsePAM no/UsePAM yes/' /config/sshd/sshd_config
11+
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /config/sshd/sshd_config
12+
echo "AuthenticationMethods password keyboard-interactive" >> /config/sshd/sshd_config
13+
echo "ChallengeResponseAuthentication yes" >> /config/sshd/sshd_config
14+
# google authenticator
15+
echo -e 'X5C4VLHDCECGFHPP3PHDCS7KAE\n" TOTP_AUTH\n55192054\n18998816\n51868999\n99315827\n42932878' > "${HOME_DIR}/.google_authenticator"
16+
chmod 0600 "${HOME_DIR}/.google_authenticator"
17+
chown "${USER_NAME}": "${HOME_DIR}/.google_authenticator"
18+
else
19+
echo "SSH MFA is disabled"
20+
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
oneshot
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/etc/s6-overlay/s6-rc.d/init-openssh-mfa-config/run

docker/ssh/linuxserverio-ssh-with-2fa-x11/rootfs/etc/s6-overlay/s6-rc.d/init-openssh-x11-config/dependencies.d/init-openssh-mfa-config

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/with-contenv bash
2+
# shellcheck shell=bash
3+
4+
echo "Enabling X11 in SSH server"
5+
6+
sed -i 's/X11Forwarding no/X11Forwarding yes/' /config/sshd/sshd_config
7+
sed -i 's/#X11DisplayOffset 10/X11DisplayOffset 10/' /config/sshd/sshd_config
8+
sed -i 's/#X11UseLocalhost yes/X11UseLocalhost yes/' /config/sshd/sshd_config
9+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
oneshot

0 commit comments

Comments
 (0)