podmansh
executes a login shell within a podman container and allows you to have a containerized platform for users to login. It’s documentation relies on Quadlet provided containers, which is great in general, but comes with one caveat: Containers are ephemeral. I wanted to have a custom sandbox for my user, where I can get even local root, but because everything is contained in a container, it never touches the base system. If a user (read: me) borks their local container, the actual OS is not affected. Plus, every user can get their own container with the system of their liking. phoenix
can use Tumbleweed, neo
can use Fedora, grisu
can use Debian, all on the same host.
In this blog post describe how I managed to get a persistent openSUSE Tumbleweed container environment for phoenix
every time I ssh onto one of my servers using podmansh
. There’s a TLDR and a detailled description top to bottom, so that you can follow the process and stop at any point, when you feel like you know what’s going on. podmansh
is conceptionally very simple and that’s what makes it elegant, but also a bit tricky for my use case.
TL;DR
Note:
phoenix
is my user and it has UID 1000. I will use the Tumbleweed container fromregistry.opensuse.org/opensuse/tumbleweed
.Replace the configuration accordingly to your own needs. Don’t be afraid to try.
As root
:
- Create a
podmansh
service starting thepodmansh
container. This service needs to be started as userphoenix
:
# /etc/systemd/user/podmansh.service
[Unit]
Description=podmansh container
After=local-fs.target
RequiresMountsFor=%t/containers
[Service]
Type=oneshot
ExecStartPre=/usr/bin/mkdir -p %h/data
ExecStartPre=-/usr/bin/podman create --name=podmansh -ti --init --user %U -w=%h --userns=keep-id -v %h/data:%h:Z -v %h/.ssh:%h/.ssh --hostname %H registry.opensuse.org/opensuse/tumbleweed sleep infinity
ExecStart=/usr/bin/podman start podmansh
RemainAfterExit=yes
[Install]
RequiredBy=default.target
Alternative: Remove the
podman create ...
from the unit and run this locally yourself.
- Edit the volume mounts (
-v
inpodman create ...
) to your liking. And the container image. And whatever else you want to modify. - Enable the systemd linger, so that the user services are started at boot time
# loginctl enable-linger 1000
As phoenix
:
- Enable the services:
$ systemctl --user daemon-reload
$ systemctl --user enable --now podmansh.service
- Ensure if the
podmansh
container is running, otherwise check why it doesn’t start (podman log podmansh
might help you)
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
79e7a706c0a0 registry.opensuse.org/opensuse/tumbleweed:latest sleep infinity 7 hours ago Up 7 hours podmansh
- Modify the container to your needs (e.g. set root password, install
sudo
, …)
$ podman exec -ti --user=0 podmansh /bin/bash
# zypper ref && zypper -n in vim sudo && visudo
As root
again (on the host, not in the container!):
- Change the login shell to
podmansh
:
# chsh -s /usr/bin/podmansh phoenix
Now the next time the user phoenix
connects via ssh to that machine, the user will be sent to their own podmansh
container. Neat!
Top to bottom
podmansh
is just a wrapper around executing podman exec -ti podmansh /bin/sh
. This means upon login, podman tries to just attach (not start or run!) to the running podmansh
container and throw/jail the user therein. It’s just awesome how little it needs to be functional, this is elegant by its own simplicity.
In practise this means all you need to have is a running podmansh
container with /bin/sh
in it. From podman 5.1 onwards you will be able to configure the container name and shell, for now it’s hardcoded but that’s in my humble opinion just a minor issue.
The documentation provides you with an example using Quadlet in /etc/containers/systemd/users/${USERID}/podmansh.container
. This is also elegant in it’s own way because this will automatically spin up a new container whenever a user session is started, and also tear it down afterwards. The downside is that Quadlet only supports ephemeral containers, so you will get a new one every session and everything you did in the last session is forgotten to the streams of time.
Having an ephemeral session container has it’s advantages. But if you’re a lazy bastard as I am, then you might want to have a persistent container because it avoids me having to setup a CI somewhere that creates a container with everything I need/want to have in it. If I want to have a new package, I just install it and it will be there the next day when I login again. I could also setup a CI that builds my own container, but that appears overkill to me for this particular task. Your mileage might vary.
My goal is to have a persistent Tumbleweed container for my phoenix
user, but with local sudo
rights so I can install whatever I need within that container without ever coming near the actual OS. If I ever bork the container, I can just throw it away and start new. The host OS won’t be bothered with whatever unholy mess phoenix
has made in his own sandbox container.
To reach that goal all we need is to make sure, that we have a podmansh
container running as user phoenix
when phoenix
logs in. I solved this goal by creating a user systemd unit, that starts the container. This together with using a systemd linger that starts a user session at boot time works nice and you have your user container ready when you login. They will be restarted after a reboot.
So, let’s first create a user systemd service:
# /etc/systemd/user/podmansh.service
[Unit]
Description=podmansh container
After=local-fs.target
RequiresMountsFor=%t/containers
[Service]
ExecStartPre=/usr/bin/mkdir -p %h/data
ExecStartPre=-/usr/bin/podman create --name=podmansh -ti --init --user %U -w=%h --userns=keep-id -v %h/data:%h:Z -v %h/.ssh:%h/.ssh --hostname %H registry.opensuse.org/opensuse/tumbleweed sleep infinity
Type=oneshot
ExecStart=/usr/bin/podman start podmansh
RemainAfterExit=yes
[Install]
RequiredBy=default.target
The only really important part is that the container needs to have the name podmansh
. Everything else you can (and should) modify to your needs. Change the mount points, the hostname, permissions, …. Whatever you like. It’s your environment.
Mind the ExecStartPre=-/usr/bin/podman create
. You can leave out this command entirely and just create the container. I like this because this means the container would be automatically re-created if it doesn’t exist. The -
at the beginning tells systemd, that it’s ok if this command fails - that’s necessary because the container will exist after the first run, and creating a container with a name that already exists results in an error (unless you use --replace
).
If you place this as /etc/systemd/user/podmansh.service
this will be available to all users. You can customize this on a per user level by putting it into /etc/systemd/user/${USERID}/podmansh.service
.
I want to be able to modify the ssh keys from within the container as well, that’s why the volume mount %h/.ssh:%h/.ssh
is included as well.
One should be in principle be able to mount the whole home directory directly using -v %h:%h
. I decided against that, because there are certain parts like the container and systemd configuration that I want to keep apart from the container data file, in case I ever need to change that. That’s why the container home directory is actually located in ~/data
. This is how I like it. You can (and should), of course, configure it however you like.
Ok, now we have the service and we can start it. Run the following commands as user phoenix
:
$ systemctl --user daemon-reload
$ systemctl --user enable --now podmansh.service
And now check if the podmansh
container is running:
$ podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
79e7a706c0a0 registry.opensuse.org/opensuse/tumbleweed:latest sleep infinity 7 hours ago Up 7 hours podmansh
Note: Don’t do
sudo -u phoenix -s
. This will not work and give you crypticcrun
errors (at least it did for me). It’s best to login directly on the system orssh
to the system (withoutpodmansh
in place).
This works for now, but there’s one problem: The container won’t be started after a reboot. Only after a user session is established. To start the container at boottime, you can enable a user lingering - this establishes a session at boot time and it then starts your container. As root you can do this for user phoenix
(UID 1000):
# loginctl enable-linger 1000
At this point all the pieces are in principle there and you could switch the shell to /usr/bin/podmansh
(or wherever podmansh
is located in your distribution).
Careful however,because you might still need to do some manual configuration on the container before it’s ready for you.
I use a fresh Tumbleweed container, where there is no root password set or sudo
installed. And since I configured the systemd service to drop me as user phoenix
and not as root
, I couldn’t run commands as root.
I resolved this by dropping me a root bash
and then installing and configuring sudo
. Because the container is persistent, I only need to do this once.
So, on the host as user phoenix
you can spawn a bash
(or whatever) shell as root (--user=0
) and do those final modifications:
$ podman exec -ti --user=0 podmansh /bin/bash
(now you're in the container, run the following there)
# zypper ref && zypper -n in vim sudo && visudo
...
#Defaults targetpw # ask for the password of the target user i.e. root
#ALL ALL=(ALL) ALL # WARNING! Only use this together with 'Defaults targetpw'!
phoenix ALL=(ALL:ALL) NOPASSWD: ALL
...
Now we’re ready to switch the user shell to podmansh
. On the host (not in the container), as root, you can switch the login shell for phoenix
to use podmansh
:
# chsh -s /usr/bin/podmansh phoenix
And now, every time I login onto that server as phoenix, I have my own persistent Tumbleweed container, where I can get local root privileges without risking to bork the host OS.
And I find this beautiful.
References
- https://docs.podman.io/en/v5.0.3/markdown/podmansh.1.html
- https://lists.podman.io/archives/list/podman@lists.podman.io/thread/PZI3ADCHR3CD44HLYM5R6GHHVRGFJ6Y4/
Credits
- Petr Lautrbach with very helpful feedback on the podman mailing list. His answer pointed me in the right direction. Thanks!