From 9a8458616c6f9b8952050f42a73f3d2f94755407 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Thu, 31 Oct 2024 22:32:00 +0100 Subject: [PATCH] containers/ws: Support using an external SSH agent In unprivileged mode, support connecting to an already running SSH agent for sharing SSH private keys. Thanks to Anton Engelhardt for the idea and initial implementation sketch! Fixes #21170 --- containers/ws/README.md | 16 ++++++++++- containers/ws/label-run | 4 ++- test/verify/check-ws-bastion | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/containers/ws/README.md b/containers/ws/README.md index bdafa4047161..b794f08dfedc 100644 --- a/containers/ws/README.md +++ b/containers/ws/README.md @@ -100,7 +100,7 @@ this by passing your own configuration as a volume: Similarly you can also provide a custom `/etc/os-release` to change the branding. -### SSH authentication +### SSH authentication: Share keys with container The login page asks the user to confirm unknown SSH host key fingerprints. You can mount your known host keys into the container at @@ -119,6 +119,20 @@ You can also mount encrypted private keys inside the container. You can set an e Private keys can be encrypted; then cockpit uses the provided password to decrypt the key. +### SSH authentication: Share SSH agent with container + +Alternatively, if you use [ssh-agent](https://linux.die.net/man/1/ssh-agent) on +your host, you can share it with the container and run the container as your +own user (*not* as system container!). Then logging into remote machines from +Cockpit's login page re-uses the loaded private keys. For that, bind-mount the +agent socket in the container and tell it its path. If your host has SELinux +enabled, you need to disable the isolation for the container, so that it is +allowed to connect to the agent socket: + + -v $SSH_AUTH_SOCK:/ssh-agent \ + -e SSH_AUTH_SOCK=/ssh-agent \ + --security-opt=label=disable + ## More Info * [Cockpit Project](https://cockpit-project.org) diff --git a/containers/ws/label-run b/containers/ws/label-run index 90297677e46e..4610d62d0e67 100755 --- a/containers/ws/label-run +++ b/containers/ws/label-run @@ -42,6 +42,8 @@ else /usr/libexec/cockpit-certificate-ensure - eval $(ssh-agent) + # start SSH agent, unless we already got pointed to one + [ -n "${SSH_AUTH_SOCK:-}" ] || eval "$(ssh-agent)" + exec /usr/libexec/cockpit-ws --local-ssh "$@" fi diff --git a/test/verify/check-ws-bastion b/test/verify/check-ws-bastion index 3d3b5edf0a67..ccd26690106d 100755 --- a/test/verify/check-ws-bastion +++ b/test/verify/check-ws-bastion @@ -255,6 +255,58 @@ class TestWsBastionContainer(testlib.MachineCase): for ssh_key_env in ["COCKPIT_SSH_KEY_PATH", f"COCKPIT_SSH_KEY_PATH_{HOST.upper()}"]: do_test_key_login(ssh_key_env=ssh_key_env) + def testExternalAgent(self): + m = self.machine + b = self.browser + + KEY_PASSWORD = "sshfoobar" + + # run the container as user -- as root does not make sense, as we want to + # share the user's SSH key with it + m.execute(f"podman save localhost/cockpit/ws -o {self.vm_tmpdir}/cockpit-ws.tar") + + # HACK: user podman does not add default route if the host doesn't have any + m.execute("ip route add default via 172.27.0.1") + self.addCleanup(m.execute, "ip route del default") + + self.restore_dir("/home/admin") + m.execute("runuser -u admin -- sh -ex", input=f""" + # we don't start a real login session here, fake it + cd $HOME + export XDG_RUNTIME_DIR=$HOME/run + mkdir -p "$XDG_RUNTIME_DIR" + + # create new key, set it up for logging into host + ssh-keygen -q -f ~/.ssh/id_bastion -N {KEY_PASSWORD} + cat ~/.ssh/id_bastion.pub > /home/admin/.ssh/authorized_keys + ssh-keyscan localhost | sed 's/^localhost/{HOST}/' > ~/.ssh/known_hosts + + # start agent, load key + eval $(ssh-agent -a $XDG_RUNTIME_DIR/ssh-agent) + printf '#!/bin/sh\necho {KEY_PASSWORD}\n' > /tmp/pwd + chmod u+x /tmp/pwd + SSH_ASKPASS_REQUIRE=force SSH_ASKPASS=/tmp/pwd ssh-add ~/.ssh/id_bastion + rm /tmp/pwd + ssh-add -l + + # run container + podman load -i {self.vm_tmpdir}/cockpit-ws.tar + podman run -d --rm --name cockpit-bastion -p 9090:9090 \ + -v ~/.ssh/known_hosts:/etc/ssh/ssh_known_hosts:ro \ + -v $XDG_RUNTIME_DIR/ssh-agent:/ssh-agent \ + -e SSH_AUTH_SOCK=/ssh-agent \ + --security-opt=label=disable \ + -e COCKPIT_DEBUG=all localhost/cockpit/ws + + until curl --fail --head -k https://localhost:9090/; do sleep 1; done + """) + + # login works without a password, using the external agent + b.open("/", tls=True) + b.set_val("#server-field", HOST) + b.try_login(password="") + b.wait_visible('#content') + @testlib.onlyImage("no cockpit/ws container on this image", "fedora-coreos") @testlib.nondestructive