Skip to content

Commit

Permalink
Sudo-less operation (#2408)
Browse files Browse the repository at this point in the history
* containerlab: Add sudoless operation.

This change introduces sudoless operations to containerlab, leveraging the SUID bit set on the binary.
The SUID-granted root privileges can optionally be gated behind a membership of the group 'clab_admins', which is set up automatically on version upgrade, adding the current Containerlab user to it.

* containerlab: Add sudoless changes to packaging+install script

* containerlab: Add missing root privilege gain to disable-tx-offload command

* containerlab: clab_admins should be added as as system group

* containerlab: Change not in user group hint to user usermod instead of gpasswd

* containerlab: Add shorthands for root UID and no-modify flags for readability

* upgrade: Fix sudoless upgrade

* containerlab: Only create clab_admins group during first upgrade/install

* docs: Add documentation about sudoless operation

* cmd: Fix broken rebase

* docs: Fix minimum version for sudoless operations support in install docs

* cmd: Allow unprivileged users to exec if they are part of the docker group

* cmd/netem: Add root requirement for show link impairments command

* format

* docs polish

* remove href from the embedded code block

* cicd, tests: Make tests run sudoless

* cmd/generate: Get root privileges for deploy action

* utils/file: Create files as running user instead of effective user

* cmd: Only run sudoless if Docker runtime is used

* runtimes: Add connectivity check for runtimes

* cmd: Don't allow non-Docker runtimes to run as root without membership check

* docs: Add note about non-privileged operations only being supported w/ Docker

* mocks: Update container runtime mock

---------

Co-authored-by: Roman Dodin <[email protected]>
  • Loading branch information
vista- and hellt authored Jan 30, 2025
1 parent d922b88 commit d604006
Show file tree
Hide file tree
Showing 68 changed files with 516 additions and 235 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ jobs:
with:
name: containerlab
- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PY_VER }}
Expand Down Expand Up @@ -226,7 +226,7 @@ jobs:
with:
name: containerlab
- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PY_VER }}
Expand Down Expand Up @@ -289,7 +289,7 @@ jobs:
with:
name: containerlab
- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PY_VER }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cisco_iol-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- uses: actions/setup-python@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/fortigate-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- uses: actions/setup-python@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/kind-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- uses: actions/setup-python@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- name: Setup Podman
if: matrix.runtime == 'podman'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/srlinux-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- name: Setup Podman
if: matrix.runtime == 'podman'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sros-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- uses: actions/setup-python@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/vxlan-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
name: containerlab

- name: Move containerlab to usr/bin
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab

- uses: actions/setup-python@v5
with:
Expand Down
4 changes: 4 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ nfpms:
postinstall: ./utils/postinstall.sh
bindir: /usr/bin
contents:
- src: ./containerlab
dst: /usr/bin/containerlab
file_info:
mode: 4755 # SUID bit set
- src: ./lab-examples
dst: /etc/containerlab/lab-examples
- src: /usr/bin/containerlab
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ ifndef suite
override suite = .
endif
robot-test: build-with-podman-debug
sudo chown root:root $(BINARY) && sudo chmod 4755 $(BINARY)
CLAB_BIN=$(BINARY) $$PWD/tests/rf-run.sh $(runtime) $$PWD/tests/$(suite)

MOCKDIR = ./mocks
Expand Down
12 changes: 12 additions & 0 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -1297,3 +1297,15 @@ func (c *CLab) Exec(ctx context.Context, cmds []string, options *ExecOptions) (*

return resultCollection, nil
}

// CheckConnectivity checks the connectivity to all container runtimes, returns an error if it encounters any, otherwise nil.
func (c *CLab) CheckConnectivity(ctx context.Context) error {
for _, r := range c.Runtimes {
err := r.CheckConnection(ctx)
if err != nil {
return fmt.Errorf("could not connect to container runtime: %v", err)
}
}

return nil
}
94 changes: 89 additions & 5 deletions cmd/common/sudo.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,100 @@
package common

import (
"errors"
"fmt"
"os"
"os/user"
"slices"

"github.com/spf13/cobra"
"golang.org/x/sys/unix"

log "github.com/sirupsen/logrus"
)

const (
CLAB_AUTHORISED_GROUP = "clab_admins"
ROOT_UID = 0
NOMODIFY = -1
)

func SudoCheck(_ *cobra.Command, _ []string) error {
id := os.Geteuid()
if id != 0 {
return errors.New("containerlab requires sudo privileges to run")
func CheckAndGetRootPrivs(_ *cobra.Command, _ []string) error {
_, euid, suid := unix.Getresuid()
if euid != 0 && suid != 0 {
return fmt.Errorf("this containerlab command requires root privileges or root via SUID to run, effective UID: %v SUID: %v", euid, suid)
}

if euid != 0 && suid == 0 {
clabGroupExists := true
clabGroup, err := user.LookupGroup(CLAB_AUTHORISED_GROUP)
if err != nil {
if _, ok := err.(user.UnknownGroupError); ok {
log.Debug("Containerlab admin group does not exist, skipping group membership check")
clabGroupExists = false
} else {
return fmt.Errorf("failed to lookup containerlab admin group: %v", err)
}
}

if clabGroupExists {
currentEffUser, err := user.Current()
if err != nil {
return err
}

effUserGroupIDs, err := currentEffUser.GroupIds()
if err != nil {
return err
}

if !slices.Contains(effUserGroupIDs, clabGroup.Gid) {
return fmt.Errorf("user '%v' is not part of containerlab admin group 'clab_admins' (GID %v), which is required to execute this command.\nTo add yourself to this group, run the following command:\n\t$ sudo gpasswd -a %v clab_admins",
currentEffUser.Username, clabGroup.Gid, currentEffUser.Username)
}

log.Debug("Group membership check passed")
}

err = obtainRootPrivs()
if err != nil {
return err
}
}

return nil
}

func obtainRootPrivs() error {
// Escalate to root privileges, changing saved UIDs to root/current group to be able to retain privilege escalation
err := changePrivileges(0, os.Getgid(), 0, os.Getgid())
if err != nil {
return err
}

log.Debug("Obtained root privileges")

return nil
}

func DropRootPrivs() error {
// Drop privileges to the running user, retaining current saved IDs
err := changePrivileges(os.Getuid(), os.Getgid(), -1, -1)
if err != nil {
return err
}

log.Debug("Dropped root privileges")

return nil
}

func changePrivileges(new_uid, new_gid, saved_uid, saved_gid int) error {
if err := unix.Setresuid(-1, new_uid, saved_uid); err != nil {
return fmt.Errorf("failed to set UID: %v", err)
}
if err := unix.Setresgid(-1, new_gid, saved_gid); err != nil {
return fmt.Errorf("failed to set GID: %v", err)
}
log.Debugf("Changed running UIDs to UID: %d GID: %d", new_uid, new_gid)
return nil
}
2 changes: 1 addition & 1 deletion cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var deployCmd = &cobra.Command{
Long: "deploy a lab based defined by means of the topology definition file\nreference: https://containerlab.dev/cmd/deploy/",
Aliases: []string{"dep"},
SilenceUsage: true,
PreRunE: common.SudoCheck,
PreRunE: common.CheckAndGetRootPrivs,
RunE: deployFn,
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var destroyCmd = &cobra.Command{
Short: "destroy a lab",
Long: "destroy a lab based defined by means of the topology definition file\nreference: https://containerlab.dev/cmd/destroy/",
Aliases: []string{"des"},
PreRunE: common.SudoCheck,
PreRunE: common.CheckAndGetRootPrivs,
RunE: destroyFn,
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/disableTxOffload.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/runtime"
"github.com/srl-labs/containerlab/utils"
)
Expand All @@ -21,6 +22,7 @@ var disableTxOffloadCmd = &cobra.Command{
Use: "disable-tx-offload",
Short: "disables tx checksum offload on eth0 interface of a container",

PreRunE: common.CheckAndGetRootPrivs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()

Expand Down
13 changes: 8 additions & 5 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/clab/exec"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/labels"
"github.com/srl-labs/containerlab/runtime"
"github.com/srl-labs/containerlab/types"
Expand All @@ -26,10 +25,9 @@ var (

// execCmd represents the exec command.
var execCmd = &cobra.Command{
Use: "exec",
Short: "execute a command on one or multiple containers",
PreRunE: common.SudoCheck,
RunE: execFn,
Use: "exec",
Short: "execute a command on one or multiple containers",
RunE: execFn,
}

func execFn(_ *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -75,6 +73,11 @@ func execFn(_ *cobra.Command, _ []string) error {
return err
}

err = c.CheckConnectivity(ctx)
if err != nil {
return err
}

var filters []*types.GenericFilter

if len(labelsFilter) != 0 {
Expand Down
5 changes: 5 additions & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/links"
"github.com/srl-labs/containerlab/nodes"
"github.com/srl-labs/containerlab/types"
Expand Down Expand Up @@ -89,6 +90,10 @@ var generateCmd = &cobra.Command{
}
}
if deploy {
err = common.CheckAndGetRootPrivs(nil, nil)
if err != nil {
return err
}
reconfigure = true
if file == "" {
file = fmt.Sprintf("%s.clab.yml", name)
Expand Down
7 changes: 5 additions & 2 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/labels"
"github.com/srl-labs/containerlab/runtime"
"github.com/srl-labs/containerlab/types"
Expand All @@ -38,7 +37,6 @@ var inspectCmd = &cobra.Command{
Short: "inspect lab details",
Long: "show details about a particular lab or all running labs\nreference: https://containerlab.dev/cmd/inspect/",
Aliases: []string{"ins", "i"},
PreRunE: common.SudoCheck,
RunE: inspectFn,
}

Expand Down Expand Up @@ -85,6 +83,11 @@ func inspectFn(_ *cobra.Command, _ []string) error {
return fmt.Errorf("could not parse the topology file: %v", err)
}

err = c.CheckConnectivity(ctx)
if err != nil {
return err
}

var containers []runtime.GenericContainer
var glabels []*types.GenericFilter

Expand Down
2 changes: 1 addition & 1 deletion cmd/redeploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var redeployCmd = &cobra.Command{
Short: "destroy and redeploy a lab",
Long: "destroy a lab and deploy it again based on the topology definition file\nreference: https://containerlab.dev/cmd/redeploy/",
Aliases: []string{"rdep"},
PreRunE: common.SudoCheck,
PreRunE: common.CheckAndGetRootPrivs,
SilenceUsage: true,
RunE: redeployFn,
}
Expand Down
13 changes: 13 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/cmd/version"
"github.com/srl-labs/containerlab/git"
"github.com/srl-labs/containerlab/utils"
Expand Down Expand Up @@ -89,6 +90,18 @@ func preRunFn(cmd *cobra.Command, _ []string) error {
// setting output to stderr, so that json outputs can be parsed
log.SetOutput(os.Stderr)

err := common.DropRootPrivs()
if err != nil {
return err
}
// Rootless operations only supported for Docker runtime
if rt != "" && rt != "docker" {
err := common.CheckAndGetRootPrivs(cmd, nil)
if err != nil {
return err
}
}

return getTopoFilePath(cmd)
}

Expand Down
2 changes: 0 additions & 2 deletions cmd/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/srl-labs/containerlab/clab"
"github.com/srl-labs/containerlab/cmd/common"
"github.com/srl-labs/containerlab/links"
"github.com/srl-labs/containerlab/nodes"
"github.com/srl-labs/containerlab/runtime"
Expand All @@ -24,7 +23,6 @@ var saveCmd = &cobra.Command{
Short: "save containers configuration",
Long: `save performs a configuration save. The exact command that is used to save the config depends on the node kind.
Refer to the https://containerlab.dev/cmd/save/ documentation to see the exact command used per node's kind`,
PreRunE: common.SudoCheck,
RunE: func(_ *cobra.Command, _ []string) error {
if name == "" && topo == "" {
return fmt.Errorf("provide topology file path with --topo flag")
Expand Down
Loading

0 comments on commit d604006

Please sign in to comment.