From 3efaffae43cb165092dedf2c8d20a9c8e59194df Mon Sep 17 00:00:00 2001 From: Lokesh Mandvekar Date: Wed, 14 Jun 2023 14:49:08 -0400 Subject: [PATCH] New command: podmansh This commit creates a new command `podmansh` command which can be used by administrators to provide a confined shell to their users. The user will only have access to the volumes and capabilities for that user. Co-authored-by: Paul Holzinger Co-authored-by: Daniel Walsh Co-authored-by: Petr Lautrbach Co-authored-by: Ed Santiago Signed-off-by: Lokesh Mandvekar --- Makefile | 9 ++- cmd/podman/containers/exec.go | 52 ++++++++++++- cmd/podman/main.go | 20 ++++- cmd/podman/registry/remote.go | 11 +++ docs/source/markdown/podmansh.1.md | 118 +++++++++++++++++++++++++++++ rpm/podman.spec | 18 +++++ test/e2e/exec_test.go | 24 ++++++ test/system/075-exec.bats | 14 ++++ 8 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 docs/source/markdown/podmansh.1.md diff --git a/Makefile b/Makefile index 0634d0cf68..c454c0ca66 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,7 @@ binaries: podman podman-remote ## Build podman and podman-remote binaries else ifneq (, $(findstring $(GOOS),darwin windows)) binaries: podman-remote ## Build podman-remote (client) only binaries else -binaries: podman podman-remote rootlessport quadlet ## Build podman, podman-remote and rootlessport binaries quadlet +binaries: podman podman-remote podmansh rootlessport quadlet ## Build podman, podman-remote and rootlessport binaries quadlet endif # Extract text following double-# for targets, as their description for @@ -408,6 +408,12 @@ bin/rootlessport: $(SOURCES) go.mod go.sum .PHONY: rootlessport rootlessport: bin/rootlessport +# podmansh calls `podman exec` into the `podmansh` container when used as +# os.Args[0] and is intended to be set as a login shell for users. +# Run: `man 1 podmansh` for details. +podmansh: bin/podman + if [ ! -f bin/podmansh ]; then ln -s podman bin/podmansh; fi + ### ### Secondary binary-build targets ### @@ -820,6 +826,7 @@ install.remote: install.bin: install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(BINDIR) install ${SELINUXOPT} -m 755 bin/podman $(DESTDIR)$(BINDIR)/podman + ln -sfr $(DESTDIR)$(BINDIR)/podman $(DESTDIR)$(BINDIR)/podmansh test -z "${SELINUXOPT}" || chcon --verbose --reference=$(DESTDIR)$(BINDIR)/podman bin/podman install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(LIBEXECPODMAN) ifneq ($(shell uname -s),FreeBSD) diff --git a/cmd/podman/containers/exec.go b/cmd/podman/containers/exec.go index 1440c93748..9b36d147e1 100644 --- a/cmd/podman/containers/exec.go +++ b/cmd/podman/containers/exec.go @@ -2,10 +2,12 @@ package containers import ( "bufio" + "context" "errors" "fmt" "os" "strings" + "time" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v4/cmd/podman/common" @@ -84,6 +86,10 @@ func execFlags(cmd *cobra.Command) { flags.StringVarP(&execOpts.WorkDir, workdirFlagName, "w", "", "Working directory inside the container") _ = cmd.RegisterFlagCompletionFunc(workdirFlagName, completion.AutocompleteDefault) + waitFlagName := "wait" + flags.Int32(waitFlagName, 0, "Total seconds to wait for container to start") + _ = flags.MarkHidden(waitFlagName) + if registry.IsRemote() { _ = flags.MarkHidden("preserve-fds") } @@ -104,7 +110,7 @@ func init() { validate.AddLatestFlag(containerExecCommand, &execOpts.Latest) } -func exec(_ *cobra.Command, args []string) error { +func exec(cmd *cobra.Command, args []string) error { var nameOrID string if len(args) == 0 && !execOpts.Latest { @@ -138,6 +144,16 @@ func exec(_ *cobra.Command, args []string) error { } } + if cmd.Flags().Changed("wait") { + seconds, err := cmd.Flags().GetInt32("wait") + if err != nil { + return err + } + if err := execWait(nameOrID, seconds); err != nil { + return err + } + } + if !execDetach { streams := define.AttachStreams{} streams.OutputStream = os.Stdout @@ -161,3 +177,37 @@ func exec(_ *cobra.Command, args []string) error { fmt.Println(id) return nil } + +func execWait(ctr string, seconds int32) error { + maxDuration := time.Duration(seconds) * time.Second + interval := 100 * time.Millisecond + + ctx, cancel := context.WithTimeout(registry.Context(), maxDuration) + defer cancel() + + cond, err := define.StringToContainerStatus("running") + if err != nil { + return err + } + waitOptions.Condition = append(waitOptions.Condition, cond) + + startTime := time.Now() + for time.Since(startTime) < maxDuration { + _, err = registry.ContainerEngine().ContainerWait(ctx, []string{ctr}, waitOptions) + if err == nil { + return nil + } + + if !errors.Is(err, define.ErrNoSuchCtr) { + return err + } + + interval *= 2 + since := time.Since(startTime) + if since+interval > maxDuration { + interval = maxDuration - since + } + time.Sleep(interval) + } + return define.ErrCanceled +} diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 6110e77fcc..0e8b2a0e1e 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -3,9 +3,10 @@ package main import ( "fmt" "os" + "path/filepath" + "strings" _ "github.com/containers/podman/v4/cmd/podman/completion" - _ "github.com/containers/podman/v4/cmd/podman/containers" _ "github.com/containers/podman/v4/cmd/podman/generate" _ "github.com/containers/podman/v4/cmd/podman/healthcheck" _ "github.com/containers/podman/v4/cmd/podman/images" @@ -27,6 +28,7 @@ import ( "github.com/containers/storage/pkg/reexec" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "golang.org/x/term" ) func main() { @@ -36,6 +38,20 @@ func main() { return } + if filepath.Base(os.Args[0]) == registry.PodmanSh || + (len(os.Args[0]) > 0 && filepath.Base(os.Args[0][1:]) == registry.PodmanSh) { + shell := strings.TrimPrefix(os.Args[0], "-") + args := []string{shell, "exec", "-i", "--wait", "10"} + if term.IsTerminal(0) || term.IsTerminal(1) || term.IsTerminal(2) { + args = append(args, "-t") + } + args = append(args, registry.PodmanSh, "/bin/sh") + if len(os.Args) > 1 { + args = append(args, os.Args[1:]...) + } + os.Args = args + } + rootCmd = parseCommands() Execute() @@ -59,7 +75,7 @@ func parseCommands() *cobra.Command { c.Command.RunE = func(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot use command %q with the %s podman client", cmd.CommandPath(), client) } - // turn of flag parsing to make we do not get flag errors + // turn off flag parsing to make we do not get flag errors c.Command.DisableFlagParsing = true // mark command as hidden so it is not shown in --help diff --git a/cmd/podman/registry/remote.go b/cmd/podman/registry/remote.go index 02aa31c580..6c75de691a 100644 --- a/cmd/podman/registry/remote.go +++ b/cmd/podman/registry/remote.go @@ -2,6 +2,8 @@ package registry import ( "os" + "path/filepath" + "strings" "sync" "github.com/containers/podman/v4/pkg/domain/entities" @@ -15,9 +17,18 @@ var remoteFromCLI = struct { sync sync.Once }{} +const PodmanSh = "podmansh" + // IsRemote returns true if podman was built to run remote or --remote flag given on CLI // Use in init() functions as an initialization check func IsRemote() bool { + // remote conflicts with podmansh in how the `-c` option gets parsed + // This is noticeable if a user with shell set to podmansh were to execute + // a command using ssh like so: + // ssh user@host id + if strings.HasSuffix(filepath.Base(os.Args[0]), PodmanSh) { + return false + } remoteFromCLI.sync.Do(func() { remote := false if _, ok := os.LookupEnv("CONTAINER_HOST"); ok { diff --git a/docs/source/markdown/podmansh.1.md b/docs/source/markdown/podmansh.1.md new file mode 100644 index 0000000000..28e10cb369 --- /dev/null +++ b/docs/source/markdown/podmansh.1.md @@ -0,0 +1,118 @@ +% podmansh 1 + +## NAME +podmansh - Execute login shell within the Podman `podmansh` container + +## SYNOPSIS +**podmansh** + +## DESCRIPTION + +Execute a user shell within a container when the user logs into the system. The container that the users get added to can be defined via a Podman Quadlet file. This user only has access to volumes and capabilities configured into the Quadlet file. + +Administrators can create a Quadlet in /etc/containers/systemd/users, which systemd will start for all users when they log in. The administrator can create a specific Quadlet with the container name `podmansh`, then enable users to use the login shell /usr/bin/podmansh. These user login shells are automatically executed inside the `podmansh` container via Podman. . + +Optionally, the administrator can place Quadlet files in the /etc/containers/systemd/users/${UID} directory for a user. Only this UID will execute these Quadlet services when that user logs in. + +The user is confined to the container environment via all of the security mechanisms, including SELinux. The only information that will be available from the system comes from volumes leaked into the container. + +Systemd will automatically create the container when the user session is started. Systemd will take down the container when all connections to the user session are removed. This means users can log in to the system multiple times, with each session connected to the same container. + +## Setup +Modify user login session using usermod + +``` +# usermod -s /usr/bin/podmansh testu +# grep testu /etc/passwd +testu:x:4004:4004::/home/testu:/usr/bin/podmansh +``` + +Now create a podman Quadlet file that looks something like one of the following. + +Fully locked down container, no access to host OS. + +``` +sudo cat > /etc/containers/systemd/users/podmansh.container << _EOF +[Unit] +Description=The podmansh container +After=local-fs.target + +[Container] +Image=registry.fedoraproject.org/fedora +ContainerName=podmansh +RemapUsers=keep-id +RunInit=yes +DropCapabilities=all +NoNewPrivileges=true + +Exec=sleep infinity + +[Install] +RequiredBy=default.target +_EOF +``` + +Users inside of this Quadlet are allowed to become root within the user namespace, and able to read/write content in their homedirectory which is mounted from a subdir `data` of the hosts users account. + +``` +sudo cat > /etc/containers/systemd/users/podmansh.container << _EOF +[Unit] +Description=The podmansh container +After=local-fs.target + +[Container] +Image=registry.fedoraproject.org/fedora +ContainerName=podmansh +RemapUsers=keep-id +RunInit=yes + +Volume=%h/data:%h:Z +Exec=sleep infinity + +[Service] +ExecStartPre=/usr/bin/mkdir -p %h/data + +[Install] +RequiredBy=default.target +_EOF +``` + +Users inside this container will be allowed to execute containers with SELinux +separate and able to read and write content in the $HOME/data directory. + +``` +sudo cat > /etc/containers/systemd/users/podmansh.container << _EOF +[Unit] +Description=The podmansh container +After=local-fs.target + +[Container] +Image=registry.fedoraproject.org/fedora +ContainerName=podmansh +RemapUsers=keep-id +RunInit=yes +PodmanArgs=--security-opt=unmask=/sys/fs/selinux \ + --security-opt=label=nested \ + --security-opt=label=user:container_user_u \ + --security-opt=label=type:container_user_t \ + --security-opt=label=role:container_user_r \ + --security-opt=label=level:s0-s0:c0.c1023 + +Volume=%h/data:%h:Z +WorkingDir=%h +Volume=/sys/fs/selinux:/sys/fs/selinux +Exec=sleep infinity + +[Service] +ExecStartPre=/usr/bin/mkdir -p %h/data + +[Install] +RequiredBy=default.target +_EOF +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-exec(1)](podman-exec.1.md)**, **quadlet(5)** + +## HISTORY +May 2023, Originally compiled by Dan Walsh diff --git a/rpm/podman.spec b/rpm/podman.spec index 042fd29152..69d7143e7b 100644 --- a/rpm/podman.spec +++ b/rpm/podman.spec @@ -241,6 +241,19 @@ It is based on the network stack of gVisor. Compared to libslirp, gvisor-tap-vsock brings a configurable DNS server and dynamic port forwarding. +%package -n %{name}sh +Summary: Confined login and user shell using %{name} +Requires: %{name} = %{epoch}:%{version}-%{release} +Provides: %{name}-shell = %{epoch}:%{version}-%{release} +Provides: %{name}-%{name}sh = %{epoch}:%{version}-%{release} + +%description -n %{name}sh +%{name}sh provides a confined login and user shell with access to volumes and +capabilities specified in user quadlets. + +It is a symlink to %{_bindir}/%{name} and execs into the `%{name}sh` container +when `%{_bindir}/%{name}sh is set as a login shell or set as os.Args[0]. + %prep %autosetup -Sgit -n %{name}-%{version} sed -i 's;@@PODMAN@@\;$(BINDIR);@@PODMAN@@\;%{_bindir};' Makefile @@ -414,6 +427,11 @@ cp -pav test/system %{buildroot}/%{_datadir}/%{name}/test/ %{_libexecdir}/%{name}/gvproxy %{_libexecdir}/%{name}/gvforwarder +%files -n %{name}sh +%license LICENSE +%doc README.md CONTRIBUTING.md install.md transfer.md +%{_bindir}/%{name}sh + %changelog %if %{with changelog} * Mon May 01 2023 RH Container Bot diff --git a/test/e2e/exec_test.go b/test/e2e/exec_test.go index dd6e3f9464..d14b02c993 100644 --- a/test/e2e/exec_test.go +++ b/test/e2e/exec_test.go @@ -548,4 +548,28 @@ RUN useradd -u 1000 auser`, fedoraMinimal) session.WaitWithDefaultTimeout() Expect(session.OutputToString()).To(Not(ContainSubstring(secretsString))) }) + + It("podman exec --wait 2 seconds on bogus container", func() { + SkipIfRemote("not supported for --wait") + session := podmanTest.Podman([]string{"exec", "--wait", "2", "1234"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + Expect(session.ErrorToString()).To(Equal("Error: cancelled by user")) + }) + + It("podman exec --wait 5 seconds for started container", func() { + SkipIfRemote("not supported for --wait") + ctrName := "waitCtr" + + session := podmanTest.Podman([]string{"exec", "--wait", "5", ctrName, "whoami"}) + + session2 := podmanTest.Podman([]string{"run", "-d", "--name", ctrName, ALPINE, "top"}) + session2.WaitWithDefaultTimeout() + + session.Wait(6) + + Expect(session2).Should(Exit(0)) + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(Equal("root")) + }) }) diff --git a/test/system/075-exec.bats b/test/system/075-exec.bats index c10c98057b..16bb8cb294 100644 --- a/test/system/075-exec.bats +++ b/test/system/075-exec.bats @@ -127,4 +127,18 @@ load helpers run_podman rm -t 0 -f $cid } +@test "podman exec --wait" { + skip_if_remote "test is meaningless over remote" + + # wait on bogus container + run_podman 125 exec --wait 5 "bogus_container" echo hello + assert "$output" = "Error: cancelled by user" + + run_podman create --name "wait_container" $IMAGE top + run_podman 255 exec --wait 5 "wait_container" echo hello + assert "$output" = "Error: can only create exec sessions on running containers: container state improper" + + run_podman rm -f wait_container +} + # vim: filetype=sh