Skip to content

Commit

Permalink
containers/ws: Support using an external SSH agent
Browse files Browse the repository at this point in the history
In unprivileged mode, support connecting to an already running SSH agent
for sharing SSH private keys.

Thanks to Anton Engelhardt <[email protected]> for the idea and
initial implementation sketch!

Fixes #21170
  • Loading branch information
martinpitt committed Dec 12, 2024
1 parent ab2d7be commit 9a84586
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 2 deletions.
16 changes: 15 additions & 1 deletion containers/ws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion containers/ws/label-run
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 52 additions & 0 deletions test/verify/check-ws-bastion
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9a84586

Please sign in to comment.