Skip to content

Commit f876df7

Browse files
Nils Barsnbars
authored andcommitted
Volume-mount container SSH keys instead of baking them into images
Container SSH public keys are now bind-mounted into student containers at /etc/ssh/master_keys/ so key rotations take effect immediately without rebuilding images or restarting containers. Also improve SSH proxy error handling: show a generic internal error with a trackable UUID instead of leaking details, and close the channel on failure.
1 parent 0789355 commit f876df7

6 files changed

Lines changed: 50 additions & 22 deletions

File tree

docker-compose.template.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ services:
9292
- {{ exercises_path }}:/exercises
9393
#Make docker availabe inside the container
9494
- /var/run/docker.sock:/var/run/docker.sock
95+
#Container SSH public keys, bind-mounted into student containers at runtime
96+
- ./container-keys:/container-keys:ro
9597
#Source for ref-utils, bind-mounted read-only into student
9698
#instances so edits on the host apply without rebuilding images.
9799
- type: bind

ref-docker-base/Dockerfile

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ RUN groupadd -g 9999 user && useradd -g 9999 -u 9999 -d /home/user -m -s /bin/ba
5959

6060
WORKDIR /root
6161

62-
COPY container-keys/root_key.pub .ssh/authorized_keys
63-
RUN chown root:root .ssh/authorized_keys \
64-
&& chmod 644 .ssh/authorized_keys
62+
RUN mkdir -p .ssh && chmod 700 .ssh
6563

6664
WORKDIR /home/user
6765

@@ -77,11 +75,11 @@ set tabsize 4
7775
set tabstospaces
7876
EOF
7977

80-
# Deploy the default ssh-key that is used for authentication by the ssh entry server as user "user".
81-
RUN mkdir .ssh
82-
COPY container-keys/user_key.pub .ssh/authorized_keys
83-
RUN chown root:root .ssh/authorized_keys \
84-
&& chmod 644 .ssh/authorized_keys
78+
RUN mkdir .ssh && chmod 700 .ssh
79+
80+
# Directory for master keys volume-mounted from the host at runtime.
81+
# sshd_config references /etc/ssh/master_keys/%u for key lookup.
82+
RUN mkdir -p /etc/ssh/master_keys
8583

8684
COPY sshd_config /etc/ssh/sshd_config
8785

ref-docker-base/sshd_config

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ StrictModes no
3636

3737
#PubkeyAuthentication yes
3838

39-
# Expect .ssh/authorized_keys2 to be disregarded by default in future.
40-
#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
39+
# Check both the per-user authorized_keys (student personal key) and the
40+
# master keys volume-mounted from the host. sshd expands %u to the
41+
# connecting username ("root" or "user").
42+
AuthorizedKeysFile .ssh/authorized_keys /etc/ssh/master_keys/%u
4143

4244
#AuthorizedPrincipalsFile none
4345

ssh-reverse-proxy/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@ futures = "0.3"
4343
# Random number generation (match russh's rand version)
4444
rand = "0.8"
4545

46+
# UUID generation for error tracking
47+
uuid = { version = "1", features = ["v4"] }
48+
4649
[dev-dependencies]
4750
tokio-test = "0.4"

ssh-reverse-proxy/src/server.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::path::Path;
1313
use std::sync::Arc;
1414
use tokio::sync::Mutex;
1515
use tracing::{debug, error, info, warn};
16+
use uuid::Uuid;
1617

1718
/// Per-connection state stored in the SSH server.
1819
pub struct ConnectionState {
@@ -529,9 +530,14 @@ impl server::Handler for SshConnection {
529530
{
530531
Ok(f) => f,
531532
Err(e) => {
532-
error!("Failed to connect to container: {}", e);
533-
let msg = format!("Error: Failed to connect to container: {}\r\n", e);
533+
let error_id = Uuid::new_v4();
534+
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
535+
let msg = format!(
536+
"Error: Internal error (id: {}). If this issue persists, please contact a supervisor and provide the error id.\r\n",
537+
error_id
538+
);
534539
session.data(channel_id, CryptoVec::from_slice(msg.as_bytes()))?;
540+
session.close(channel_id)?;
535541
return Ok(());
536542
}
537543
};
@@ -620,8 +626,10 @@ impl server::Handler for SshConnection {
620626
{
621627
Ok(f) => f,
622628
Err(e) => {
623-
error!("Failed to connect to container: {}", e);
629+
let error_id = Uuid::new_v4();
630+
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
624631
session.channel_failure(channel_id)?;
632+
session.close(channel_id)?;
625633
return Ok(());
626634
}
627635
};
@@ -689,8 +697,10 @@ impl server::Handler for SshConnection {
689697
{
690698
Ok(f) => f,
691699
Err(e) => {
692-
error!("Failed to connect to container: {}", e);
700+
let error_id = Uuid::new_v4();
701+
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
693702
session.channel_failure(channel_id)?;
703+
session.close(channel_id)?;
694704
return Ok(());
695705
}
696706
};

webapp/ref/core/instance.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,26 @@ def start(self):
584584
"mode": "ro",
585585
}
586586

587+
# Bind-mount container SSH public keys so sshd can authenticate the
588+
# SSH reverse proxy. Keys are live — changing them on the host takes
589+
# effect immediately without rebuilding images or restarting containers.
590+
container_keys_path = Path("/container-keys")
591+
user_key = container_keys_path / "user_key.pub"
592+
root_key = container_keys_path / "root_key.pub"
593+
if not user_key.exists() or not root_key.exists():
594+
raise RuntimeError(
595+
f"Container SSH public keys not found at {container_keys_path}. "
596+
"Ensure container-keys/ is mounted into the web container."
597+
)
598+
mounts[self.dc.local_path_to_host(str(user_key))] = {
599+
"bind": "/etc/ssh/master_keys/user",
600+
"mode": "ro",
601+
}
602+
mounts[self.dc.local_path_to_host(str(root_key))] = {
603+
"bind": "/etc/ssh/master_keys/root",
604+
"mode": "ro",
605+
}
606+
587607
# Coverage configuration for testing
588608
coverage_env = {}
589609
if os.environ.get("COVERAGE_PROCESS_START"):
@@ -632,17 +652,10 @@ def start(self):
632652

633653
instance_entry_service.container_id = container.id
634654

635-
# Scrip that is initially executed to setup the environment.
636-
# 1. Add the SSH key of the user that owns the container to authorized_keys.
637-
# FIXME: This key is not actually used for anything right now, since the ssh entry server
638-
# uses the master key (docker base image authorized_keys) for authentication for all containers.
639-
# 2. Store the instance ID as string in a file /etc/instance_id.
655+
# Script that is initially executed to setup the environment.
640656
container_setup_script = (
641657
"#!/bin/bash\n"
642658
"set -e\n"
643-
f'if ! grep -q "{self.instance.user.pub_key}" /home/user/.ssh/authorized_keys; then\n'
644-
f'bash -c "echo {self.instance.user.pub_key} >> /home/user/.ssh/authorized_keys"\n'
645-
"fi\n"
646659
f"echo -n {self.instance.id} > /etc/instance_id && chmod 400 /etc/instance_id\n"
647660
)
648661
if exercise.entry_service.disable_aslr:

0 commit comments

Comments
 (0)