| 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.
SSH / console login
│
▼
/usr/bin/podmansh
│
▼
user-specific Podman Quadlet
│
▼
podmansh container
│
▼
confined shell environmentThat 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.
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:
command -v podmanshSample output on a host where the subpackage is installed:
/usr/bin/podmanshOn RHEL 10.2 without the subpackage, command -v prints nothing. Create the same symlink Fedora uses:
sudo ln -sf /usr/bin/podman /usr/bin/podmanshVerify the link:
ls -la /usr/bin/podmanshSample output:
lrwxrwxrwx. 1 root root 15 Aug 23 01:26 /usr/bin/podmansh -> /usr/bin/podmanRecord the Podman version because older distributions may lack podmansh support entirely:
podman --versionSample output:
podman version 5.8.2When /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.
sudo useradd -m -s /usr/bin/podmansh lockeduConfirm the shell field in /etc/passwd:
getent passwd lockeduSample output:
lockedu:x:1019:1020::/home/lockedu:/usr/bin/podmanshThe 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:
sudo mkdir -p /etc/containers/systemd/users/1019Write the Quadlet:
[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.targetKey directives:
ContainerName=podmansh— matches the defaultpodmanshcontainer name; override both this and[podmansh] containerincontainers.confif 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=allandNoNewPrivileges=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:
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman pull registry.access.redhat.com/ubi9/ubi-minimalFrom 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:
su - lockedu -c 'id'Sample 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:
su - lockedu -c 'cat /etc/os-release | head -2'Sample 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:
sudo sh -c 'echo host-only-marker > /root/podmansh-host-secret.txt'Try to read it from the confined session:
su - lockedu -c 'cat /root/podmansh-host-secret.txt'Sample output:
cat: /root/podmansh-host-secret.txt: Permission deniedThe 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:
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman ps --format '{{.Names}} {{.Status}}'Sample output:
podmansh Up 2 minutesThe 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:
su - lockedu -c 'echo session1; sleep 60' &
su - lockedu -c 'echo session2; sleep 60' &While both are alive:
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait podman ps --format '{{.Names}}'Sample output:
podmanshBoth 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:
sudo useradd -m -s /usr/bin/podmansh confineduPlace a marker file on the host:
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/dataWrite a level 2 Quadlet that bind-mounts only ~/data to the container home:
[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.targetInstall 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:
su - confinedu -c 'pwd && cat marker.txt'Sample output:
/home/confinedu
persisted-from-hostWrite from inside the session:
su - confinedu -c 'echo appended-in-container >> marker.txt'On the host, the bind-mounted file shows both lines:
cat /home/confinedu/data/marker.txtSample output:
persisted-from-host
appended-in-containerPaths 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:
[podmansh]
#timeout = 30The 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:
grep -A8 '^\[podmansh\]' /usr/share/containers/containers.confSample 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 = 30Override 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:
getenforceSample output:
EnforcingInside a confined session, the process context shows a container domain:
su - lockedu -c 'cat /proc/self/attr/current'Sample output:
system_u:system_r:container_t:s0:c274,c972The 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:
- User logs in — user manager starts
podmansh.serviceif it is not already running. podmanshattaches the login shell to the running container.- 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:
loginctl show-user lockedu -p LingerSample output when lingering is disabled (default):
Linger=noAfter the user's sessions end, verify whether the container stopped:
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:
sudo usermod -s /bin/bash lockeduConfirm the change:
getent passwd lockeduSample output:
lockedu:x:1019:1020::/home/lockedu:/bin/bashThe user can log in on the host again while you repair the Quadlet.
Inspect what systemd generated for the user:
sudo systemd-run --machine=lockedu@ --quiet --user --collect --pipe --wait /usr/libexec/podman/quadlet -user -dryrunSample output (truncated):
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:
sudo machinectl shell lockedu@ /bin/true 2>/dev/null || true
journalctl _UID=1019 -u podmansh.service --no-pager -n 20On 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
podman exec
→ administrator or user explicitly enters an already running container
podmansh
→ login shell automatically places the user into their configured containerpodman 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
- podmansh(1) — Podman documentation
- podman-systemd.unit(5) — Quadlet unit reference
- containers.conf(5) — podmansh table
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.

