`podmansh`: Confine Linux Users to a Podman Container Shell

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Any Linux host with Podman installed
Privilege sudo or root to create users and system Quadlets; confined users run rootless Podman through their login session
Scope podmansh login shell setup with per-UID Quadlet files, locked and persistent confinement examples, session lifecycle, podmansh timeout in containers.conf, SELinux behavior on RHEL, and shell recovery. Does not cover full Quadlet syntax, rootless subuid provisioning, SSH server setup, or nested containers.
Related guides Rootless vs rootful Podman
Install Podman on RHEL

podmansh answers a narrow admin question: how do you make a Linux account land inside a controlled Podman container instead of an unrestricted host shell? The host still owns authentication, but the user's login shell becomes /usr/bin/podmansh, which attaches to a Quadlet-managed container named podmansh.

text
SSH / console login
/usr/bin/podmansh
user-specific Podman Quadlet
podmansh container
confined shell environment

That is different from Run commands with podman exec because podmansh is integrated into the account login shell, not a one-off command you type after you already have a host shell.

IMPORTANT
Always test podmansh on a disposable account before changing a production user's shell. If the Quadlet is missing or invalid, login fails with a timeout. As root, restore access with usermod -s /bin/bash username — the full recovery walkthrough is under Recover when the Quadlet breaks.

What is podmansh?

podmansh is a login shell entry point that execs into a Podman container. Administrators define that container in a Quadlet file; systemd's user manager starts it when the account logs in.

The confined user only sees host resources explicitly mounted into the Quadlet:

  • Volumes you declare with Volume= (or related keys) appear inside the container.
  • Capabilities, SELinux labels, and user-namespace settings from the Quadlet apply to every login session.
  • Unmounted host paths are not visible unless the image or configuration leaks them.

By default, podmansh joins a container named podmansh; [podmansh] container in containers.conf can override that name. That name ties the login shell to the Quadlet unit podmansh.service. Quadlet fundamentals live in Podman Quadlet with systemd; this guide only shows the podmansh.container shape you need for confinement.


Check whether podmansh is installed

On Fedora and some rolling distributions, podmansh ships as a subpackage that installs /usr/bin/podmansh as a symlink to podman. RHEL 10.2 in this lab ships Podman 5.8.2 without a separate podmansh RPM, so the binary may be missing even though Podman supports the feature.

Confirm the path exists:

bash
command -v podmansh

Sample output on a host where the subpackage is installed:

output
/usr/bin/podmansh

On RHEL 10.2 without the subpackage, command -v prints nothing. Create the same symlink Fedora uses:

bash
sudo ln -sf /usr/bin/podman /usr/bin/podmansh

Verify the link:

bash
ls -la /usr/bin/podmansh

Sample output:

output
lrwxrwxrwx. 1 root root 15 Aug 23 01:26 /usr/bin/podmansh -> /usr/bin/podman

Record the Podman version because older distributions may lack podmansh support entirely:

bash
podman --version

Sample output:

output
podman version 5.8.2

When /usr/bin/podmansh is set as a login shell, invoking podmansh --help from an existing session tries to join the podmansh container rather than printing help. Use man podmansh or the upstream docs for reference instead.


Create a user with podmansh as the login shell

Create a dedicated test account. Do not point an administrator account at podmansh while you are still learning the workflow.

bash
sudo useradd -m -s /usr/bin/podmansh lockedu

Confirm the shell field in /etc/passwd:

bash
getent passwd lockedu

Sample output:

output
lockedu:x:1019:1020::/home/lockedu:/usr/bin/podmansh

The trailing /usr/bin/podmansh is what PAM and SSH use when the account logs in.


Where the podmansh Quadlet lives

Quadlet files for per-user login containers use two supported layouts:

Path Scope
/etc/containers/systemd/users/ Quadlets started for every user on login
/etc/containers/systemd/users/${UID}/ Quadlets only for that UID

A file such as /etc/containers/systemd/users/1019/podmansh.container applies only to UID 1019. That per-UID path is ideal for graded confinement: one locked-down contractor account can run a minimal image while another user gets a home-directory bind mount on the same host.

podmansh.container is the conventional filename used by the upstream examples. With the default [podmansh] container="podmansh" setting, the generated container must be named podmansh, so use ContainerName=podmansh. If you override [podmansh] container, make the Quadlet create that configured container name instead.


Create a fully confined podmansh.container

This example is level 1 confinement: no host volumes, dropped capabilities, and keep-id user namespace mapping. The image is UBI 9 minimal, which matches other Podman articles on this site.

Replace 1019 with the UID from id -u lockedu:

bash
sudo mkdir -p /etc/containers/systemd/users/1019

Write the Quadlet:

text
[Unit]
Description=The podmansh container
After=local-fs.target

[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
ContainerName=podmansh
UserNS=keep-id
RunInit=yes
DropCapability=all
NoNewPrivileges=true
Exec=sleep infinity

[Install]
RequiredBy=default.target

Key directives:

  • ContainerName=podmansh — matches the default podmansh container name; override both this and [podmansh] container in containers.conf if you choose a different name.
  • UserNS=keep-id — maps the host user's UID/GID into the container; see Podman user namespaces for mapping detail.
  • RunInit=yes — runs an init process so session teardown behaves predictably.
  • DropCapability=all and NoNewPrivileges=true — strip Linux capabilities and block privilege escalation inside the container.
  • Exec=sleep infinity — keeps the container running while users attach shells.

Do not enable lingering unless you intentionally want the user's systemd manager and podmansh service to remain active after the last login ends. Normal podmansh operation starts the user Quadlet with the login session. Quadlet user files in /etc/containers/systemd/users/${UID}/ are loaded when that user's login session starts — linger is not required for basic login operation.

Pre-pull the image into the user's rootless storage so the first login does not wait on a registry pull. For admin-side commands, run Podman through a real user systemd session — plain sudo -u USER podman can lack XDG_RUNTIME_DIR and break rootless state:

bash
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman pull registry.access.redhat.com/ubi9/ubi-minimal

From an actual login as lockedu, you can run the same podman commands directly.

The pull completes silently except for layer progress lines; when it finishes, podman images in that user's session lists the UBI minimal image.


Test the confined login

Log in as the test user from a separate session. On the lab host, su - exercises the same login shell path:

bash
su - lockedu -c 'id'

Sample output:

output
uid=1019(lockedu) gid=1020(lockedu) groups=1020(lockedu)

Inside the container namespace, the account keeps its host UID thanks to keep-id.

Check which OS tree the session sees:

bash
su - lockedu -c 'cat /etc/os-release | head -2'

Sample output:

output
NAME="Red Hat Enterprise Linux"
VERSION="9.8 (Plow)"

That is the container image userspace, not necessarily the host /etc/os-release.

UBI minimal does not ship hostname or mount, but you can still confirm isolation. Create a host-only marker file:

bash
sudo sh -c 'echo host-only-marker > /root/podmansh-host-secret.txt'

Try to read it from the confined session:

bash
su - lockedu -c 'cat /root/podmansh-host-secret.txt'

Sample output:

output
cat: /root/podmansh-host-secret.txt: Permission denied

The path is not bind-mounted into the container, so the confined user cannot read host /root content.

From the host, inspect the running container in the user's rootless context:

bash
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman ps --format '{{.Names}} {{.Status}}'

Sample output:

output
podmansh Up 2 minutes

The container name matches the Quadlet ContainerName value.


Multiple login sessions share one container

Upstream behavior ties one podmansh container to each user:

  • systemd creates the container when the user's session starts.
  • Additional logins for the same UID attach to the same running container.
  • systemd removes the container after all sessions for that user end (see logout notes below).

Open two overlapping sessions and count containers:

bash
su - lockedu -c 'echo session1; sleep 60' &
su - lockedu -c 'echo session2; sleep 60' &

While both are alive:

bash
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman ps --format '{{.Names}}'

Sample output:

output
podmansh

Both login shells overlap for 60 seconds, but only one podmansh container exists. Both sessions attach to that same per-user container. That differs from giving every SSH connection its own podman run instance.


Expose only selected host data

Confinement becomes tangible when you mount one host directory and leave everything else hidden.

Create a host data directory for a second test user:

bash
sudo useradd -m -s /usr/bin/podmansh confinedu

Place a marker file on the host:

bash
sudo mkdir -p /home/confinedu/data
sudo sh -c 'echo persisted-from-host > /home/confinedu/data/marker.txt'
sudo chown -R confinedu:confinedu /home/confinedu/data

Write a level 2 Quadlet that bind-mounts only ~/data to the container home:

text
[Unit]
Description=The podmansh container
After=local-fs.target

[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
ContainerName=podmansh
UserNS=keep-id
RunInit=yes
Volume=%h/data:%h:Z
WorkingDir=%h
Exec=sleep infinity

[Service]
ExecStartPre=/usr/bin/mkdir -p %h/data

[Install]
RequiredBy=default.target

Install it under /etc/containers/systemd/users/$(id -u confinedu)/podmansh.container and pre-pull the image the same way you did for lockedu.

Log in and read the mounted file:

bash
su - confinedu -c 'pwd && cat marker.txt'

Sample output:

output
/home/confinedu
persisted-from-host

Write from inside the session:

bash
su - confinedu -c 'echo appended-in-container >> marker.txt'

On the host, the bind-mounted file shows both lines:

bash
cat /home/confinedu/data/marker.txt

Sample output:

output
persisted-from-host
appended-in-container

Paths outside the mount remain blocked. An attempt to read unmounted host content returns permission errors, the same pattern you saw with /root on the locked account.


Give the user a persistent home directory

Without a host bind mount or named volume, files written to the container writable layer follow the container lifecycle. When the last session ends and systemd stops podmansh.service, that ephemeral layer disappears.

Level 2 addresses persistence by mounting %h/data to %h inside the container and setting WorkingDir=%h. The user works in a host-backed directory; only that tree survives logout.

Decide explicitly what should persist:

Data Typical approach
Project files Volume=%h/data:%h:Z or a named volume
SSH keys inside the container Usually avoid; keep keys on the host or in a dedicated mount
Package installs in the image Rebuild the image or use a persistent mount for /var if truly needed

This article does not reproduce the full Podman volumes guide — only the podmansh-specific mount pattern.


Three useful podmansh confinement levels

Level Goal Quadlet highlights
1 — Locked shell Minimal contained shell, no host files DropCapability=all, NoNewPrivileges=true, no Volume= lines, small image
2 — Persistent files Same isolation, selected host directories Volume=%h/data:%h:Z, WorkingDir=%h, optional ExecStartPre to create the host path
3 — Nested Podman User runs containers inside the login container PodmanArgs with nested SELinux labels, /sys/fs/selinux mount, extra capabilities

Levels 1 and 2 were exercised on the lab host with lockedu and confinedu. Level 3 follows upstream examples in podmansh(1) with PodmanArgs=--security-opt=label=nested and a Fedora image; it requires additional SELinux policy tuning and was not fully reproduced in this RHEL 10.2 walkthrough. Treat nested Podman as advanced and optional — it is not a requirement for basic podmansh confinement.


Configure podmansh timeout

podmansh waits for the Quadlet container to become ready before attaching your shell. If the service never reaches a running state, login fails with a timeout error.

Current Podman stores the setting in the [podmansh] table of containers.conf:

text
[podmansh]
#timeout = 30

The default is 30 seconds. The older engine.podmansh_timeout key is deprecated in favor of [podmansh] timeout. The same [podmansh] table can override the shell spawned inside the container and the container name; the defaults are /bin/sh and podmansh. This guide keeps those defaults.

To inspect the effective default on your host:

bash
grep -A8 '^\[podmansh\]' /usr/share/containers/containers.conf

Sample output:

output
[podmansh]
# Shell to spawn in container. Default: /bin/sh.
#shell = "/bin/sh"
#
# Name of the container the podmansh user should join.
#container = "podmansh"
#
# Default timeout in seconds for podmansh logins.
# Favored over the deprecated "podmansh_timeout" field.
#timeout = 30

Override only when you have measured a need — for example a slow registry on first pull. A copy in /etc/containers/containers.conf might set timeout = 120 under [podmansh]. Pre-pulling the image for each confined UID avoids most timeout failures in practice.


SELinux confinement on RHEL

On RHEL-family hosts with SELinux enforcing, podmansh users remain constrained through normal container SELinux mechanisms. Only explicitly mounted and labeled host content becomes accessible to the container process.

Confirm enforcing mode on the host:

bash
getenforce

Sample output:

output
Enforcing

Inside a confined session, the process context shows a container domain:

bash
su - lockedu -c 'cat /proc/self/attr/current'

Sample output:

output
system_u:system_r:container_t:s0:c274,c972

The container_t type is expected for processes inside the podmansh container. Volume mounts that use the :Z suffix apply a private label so the confined user can read and write host-backed files without exposing unrelated host paths.


What happens when the user logs out?

The lifecycle is driven by the user systemd session:

  1. User logs in — user manager starts podmansh.service if it is not already running.
  2. podmansh attaches the login shell to the running container.
  3. User logs out of all sessions — user manager stops services tied to that session.

With normal session management and lingering disabled, systemd takes down the podmansh container after the user's remaining login sessions disappear. If lingering is enabled intentionally, the user manager can remain active and the container may persist after logout.

Check whether lingering is enabled for a confined account:

bash
loginctl show-user lockedu -p Linger

Sample output when lingering is disabled (default):

output
Linger=no

After the user's sessions end, verify whether the container stopped:

bash
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman ps --format '{{.Names}}'

If the list is empty, the container exited with the session. A new login starts a fresh podmansh instance.


Recover when the Quadlet breaks

A bad Quadlet file or missing image leaves the user unable to log in. Keep root access and fix the account before debugging Quadlet syntax.

Restore a normal shell:

bash
sudo usermod -s /bin/bash lockedu

Confirm the change:

bash
getent passwd lockedu

Sample output:

output
lockedu:x:1019:1020::/home/lockedu:/bin/bash

The user can log in on the host again while you repair the Quadlet.

Inspect what systemd generated for the user:

bash
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait /usr/libexec/podman/quadlet -user -dryrun

Sample output (truncated):

output
quadlet-generator[217398]: Loading source unit file /etc/containers/systemd/users/1019/podmansh.container
---podmansh.service---
[Unit]
Description=The podmansh container
...
[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal
ContainerName=podmansh
...

Read the user journal when the service fails:

bash
sudo machinectl shell lockedu@ /bin/true 2>/dev/null || true
journalctl _UID=1019 -u podmansh.service --no-pager -n 20

On the lab host, a failed image unpack with no space left on device showed up in that journal — free disk space on / before testing additional confined users.


podmansh vs SSH into a container

Topic podmansh SSH daemon in container
Authentication Host PAM / SSH validates the Linux account Container runs its own sshd and user database
Entry point Login shell /usr/bin/podmansh Network service inside the container image
Lifecycle systemd user manager + Quadlet Container orchestration you manage separately
Host confinement Host account exists; shell is containerized Useful when the workload is designed as an SSH appliance
Typical use Restrict shared-login or contractor accounts on a host Purpose-built images that intentionally expose SSH

SSH inside a container is not always wrong — it fits images built for remote administration. podmansh fits when the host must own the account but the interactive environment should stay inside Podman.


podmansh vs podman exec

text
podman exec
    → administrator or user explicitly enters an already running container

podmansh
    → login shell automatically places the user into their configured container

podman exec assumes you already have host access and a running container name. podmansh is for accounts that should never receive an unrestricted host shell in the first place.


Troubleshooting

Symptom Likely cause Fix
Error: timed out waiting for container: podmansh Quadlet missing, service failed, or image not ready Run quadlet -user -dryrun as the user; check journalctl for podmansh.service; pre-pull the image
command -v podmansh empty on RHEL podmansh subpackage not installed ln -sf /usr/bin/podman /usr/bin/podmansh or install the distro podmansh package when available
Login works but host files visible Unintended bind mounts or overly permissive Quadlet Remove extra Volume= lines; verify DropCapability=all for locked profiles
Second confined user cannot log in Disk full during image pull Free space under / and pre-pull as that UID
Container survives logout Lingering enabled or active user manager Disable linger if policy requires teardown after last session; stop podmansh.service manually if needed

References


Summary

podmansh turns a Linux login shell into a doorway to a Quadlet-defined Podman container. You set /usr/bin/podmansh in /etc/passwd, place podmansh.container under /etc/containers/systemd/users/${UID}/, and name the container podmansh so the shell can find it. On the lab host, a fully locked account saw only the UBI image tree while a second account read and wrote a single bind-mounted ~/data directory on the host.

Session behavior matters for operations: multiple logins for one user share the same container, and systemd normally takes it down after the user's sessions end. Deliberately enabled lingering can keep the user manager—and the Quadlet service—running after logout. SELinux enforcing mode on RHEL kept the confined process in container_t, and only :Z-labeled mounts bridged host files into the session.

Before you assign podmansh to a real account, pre-pull images per UID, test on throwaway users, and keep usermod -s /bin/bash ready when a Quadlet mistake would otherwise lock someone out. For Quadlet syntax beyond this login-shell pattern, continue with Podman Quadlet with systemd.


Frequently Asked Questions

1. What is podmansh?

podmansh is a login shell that places a user session inside a Podman container defined by a Quadlet file named podmansh.container. The host still authenticates the account through PAM or SSH, but the interactive shell runs in the container namespace with only the volumes and capabilities the Quadlet exposes.

2. Does every SSH login create a new podmansh container?

No. systemd starts one podmansh container per user when that user's session begins. Additional logins for the same account attach to the same running container. By default, systemd takes the container down after the user's sessions end. If the administrator deliberately enables lingering, the user manager—and therefore the Quadlet service—can remain active after logout.

3. Why does podmansh time out on login?

podmansh waits for the Quadlet-managed container to become ready. A missing Quadlet file, image pull failure, invalid Volume syntax, or disk space exhaustion can prevent podmansh.service from starting. Check journalctl for the user manager and verify the image is already pulled for that UID's storage.

4. Can I lock myself out by setting podmansh as my shell?

Yes, if the Quadlet is missing or broken. Always test on a disposable account first. As root, restore access with usermod -s /bin/bash username before debugging the Quadlet file.

5. How is podmansh different from podman exec?

podman exec is an explicit command an administrator or user runs against an already running container. podmansh is wired into the account login shell field in /etc/passwd, so every console or SSH login automatically enters the configured podmansh container without a separate exec step.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)