Skip to content

Commit

Permalink
Merge pull request containers#18739 from lsm5/podmansh-exec-3
Browse files Browse the repository at this point in the history
New command: podmansh
  • Loading branch information
openshift-merge-robot authored Jun 15, 2023
2 parents eb5c0b5 + 3efaffa commit 1f18ad5
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 4 deletions.
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
###
Expand Down Expand Up @@ -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)
Expand Down
52 changes: 51 additions & 1 deletion cmd/podman/containers/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
20 changes: 18 additions & 2 deletions cmd/podman/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand All @@ -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()
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions cmd/podman/registry/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package registry

import (
"os"
"path/filepath"
"strings"
"sync"

"github.com/containers/podman/v4/pkg/domain/entities"
Expand All @@ -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 {
Expand Down
118 changes: 118 additions & 0 deletions docs/source/markdown/podmansh.1.md
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
18 changes: 18 additions & 0 deletions rpm/podman.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <[email protected]>
Expand Down
24 changes: 24 additions & 0 deletions test/e2e/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
})
14 changes: 14 additions & 0 deletions test/system/075-exec.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 1f18ad5

Please sign in to comment.