Skip to content

Commit d604006

Browse files
vista-hellt
andauthored
Sudo-less operation (#2408)
* 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 <dodin.roman@gmail.com>
1 parent d922b88 commit d604006

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+516
-235
lines changed

.github/workflows/cicd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ jobs:
173173
with:
174174
name: containerlab
175175
- name: Move containerlab to usr/bin
176-
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
176+
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
177177
- uses: actions/setup-python@v5
178178
with:
179179
python-version: ${{ env.PY_VER }}
@@ -226,7 +226,7 @@ jobs:
226226
with:
227227
name: containerlab
228228
- name: Move containerlab to usr/bin
229-
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
229+
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
230230
- uses: actions/setup-python@v5
231231
with:
232232
python-version: ${{ env.PY_VER }}
@@ -289,7 +289,7 @@ jobs:
289289
with:
290290
name: containerlab
291291
- name: Move containerlab to usr/bin
292-
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab
292+
run: sudo mv ./containerlab /usr/bin/containerlab && sudo chown root:root /usr/bin/containerlab && sudo chmod 4755 /usr/bin/containerlab
293293
- uses: actions/setup-python@v5
294294
with:
295295
python-version: ${{ env.PY_VER }}

.github/workflows/cisco_iol-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
name: containerlab
2828

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

3232
- uses: actions/setup-python@v5
3333
with:

.github/workflows/fortigate-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
name: containerlab
2828

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

3232
- uses: actions/setup-python@v5
3333
with:

.github/workflows/kind-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
name: containerlab
2828

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

3232
- uses: actions/setup-python@v5
3333
with:

.github/workflows/smoke-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
name: containerlab
5252

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

5656
- name: Setup Podman
5757
if: matrix.runtime == 'podman'

.github/workflows/srlinux-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
name: containerlab
3232

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

3636
- name: Setup Podman
3737
if: matrix.runtime == 'podman'

.github/workflows/sros-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
name: containerlab
2929

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

3333
- uses: actions/setup-python@v5
3434
with:

.github/workflows/vxlan-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
name: containerlab
3030

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

3434
- uses: actions/setup-python@v5
3535
with:

.goreleaser.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ nfpms:
9797
postinstall: ./utils/postinstall.sh
9898
bindir: /usr/bin
9999
contents:
100+
- src: ./containerlab
101+
dst: /usr/bin/containerlab
102+
file_info:
103+
mode: 4755 # SUID bit set
100104
- src: ./lab-examples
101105
dst: /etc/containerlab/lab-examples
102106
- src: /usr/bin/containerlab

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ifndef suite
6464
override suite = .
6565
endif
6666
robot-test: build-with-podman-debug
67+
sudo chown root:root $(BINARY) && sudo chmod 4755 $(BINARY)
6768
CLAB_BIN=$(BINARY) $$PWD/tests/rf-run.sh $(runtime) $$PWD/tests/$(suite)
6869

6970
MOCKDIR = ./mocks

clab/clab.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,3 +1297,15 @@ func (c *CLab) Exec(ctx context.Context, cmds []string, options *ExecOptions) (*
12971297

12981298
return resultCollection, nil
12991299
}
1300+
1301+
// CheckConnectivity checks the connectivity to all container runtimes, returns an error if it encounters any, otherwise nil.
1302+
func (c *CLab) CheckConnectivity(ctx context.Context) error {
1303+
for _, r := range c.Runtimes {
1304+
err := r.CheckConnection(ctx)
1305+
if err != nil {
1306+
return fmt.Errorf("could not connect to container runtime: %v", err)
1307+
}
1308+
}
1309+
1310+
return nil
1311+
}

cmd/common/sudo.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,100 @@
11
package common
22

33
import (
4-
"errors"
4+
"fmt"
55
"os"
6+
"os/user"
7+
"slices"
68

79
"github.com/spf13/cobra"
10+
"golang.org/x/sys/unix"
11+
12+
log "github.com/sirupsen/logrus"
13+
)
14+
15+
const (
16+
CLAB_AUTHORISED_GROUP = "clab_admins"
17+
ROOT_UID = 0
18+
NOMODIFY = -1
819
)
920

10-
func SudoCheck(_ *cobra.Command, _ []string) error {
11-
id := os.Geteuid()
12-
if id != 0 {
13-
return errors.New("containerlab requires sudo privileges to run")
21+
func CheckAndGetRootPrivs(_ *cobra.Command, _ []string) error {
22+
_, euid, suid := unix.Getresuid()
23+
if euid != 0 && suid != 0 {
24+
return fmt.Errorf("this containerlab command requires root privileges or root via SUID to run, effective UID: %v SUID: %v", euid, suid)
25+
}
26+
27+
if euid != 0 && suid == 0 {
28+
clabGroupExists := true
29+
clabGroup, err := user.LookupGroup(CLAB_AUTHORISED_GROUP)
30+
if err != nil {
31+
if _, ok := err.(user.UnknownGroupError); ok {
32+
log.Debug("Containerlab admin group does not exist, skipping group membership check")
33+
clabGroupExists = false
34+
} else {
35+
return fmt.Errorf("failed to lookup containerlab admin group: %v", err)
36+
}
37+
}
38+
39+
if clabGroupExists {
40+
currentEffUser, err := user.Current()
41+
if err != nil {
42+
return err
43+
}
44+
45+
effUserGroupIDs, err := currentEffUser.GroupIds()
46+
if err != nil {
47+
return err
48+
}
49+
50+
if !slices.Contains(effUserGroupIDs, clabGroup.Gid) {
51+
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",
52+
currentEffUser.Username, clabGroup.Gid, currentEffUser.Username)
53+
}
54+
55+
log.Debug("Group membership check passed")
56+
}
57+
58+
err = obtainRootPrivs()
59+
if err != nil {
60+
return err
61+
}
62+
}
63+
64+
return nil
65+
}
66+
67+
func obtainRootPrivs() error {
68+
// Escalate to root privileges, changing saved UIDs to root/current group to be able to retain privilege escalation
69+
err := changePrivileges(0, os.Getgid(), 0, os.Getgid())
70+
if err != nil {
71+
return err
72+
}
73+
74+
log.Debug("Obtained root privileges")
75+
76+
return nil
77+
}
78+
79+
func DropRootPrivs() error {
80+
// Drop privileges to the running user, retaining current saved IDs
81+
err := changePrivileges(os.Getuid(), os.Getgid(), -1, -1)
82+
if err != nil {
83+
return err
84+
}
85+
86+
log.Debug("Dropped root privileges")
87+
88+
return nil
89+
}
90+
91+
func changePrivileges(new_uid, new_gid, saved_uid, saved_gid int) error {
92+
if err := unix.Setresuid(-1, new_uid, saved_uid); err != nil {
93+
return fmt.Errorf("failed to set UID: %v", err)
94+
}
95+
if err := unix.Setresgid(-1, new_gid, saved_gid); err != nil {
96+
return fmt.Errorf("failed to set GID: %v", err)
1497
}
98+
log.Debugf("Changed running UIDs to UID: %d GID: %d", new_uid, new_gid)
1599
return nil
16100
}

cmd/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ var deployCmd = &cobra.Command{
5656
Long: "deploy a lab based defined by means of the topology definition file\nreference: https://containerlab.dev/cmd/deploy/",
5757
Aliases: []string{"dep"},
5858
SilenceUsage: true,
59-
PreRunE: common.SudoCheck,
59+
PreRunE: common.CheckAndGetRootPrivs,
6060
RunE: deployFn,
6161
}
6262

cmd/destroy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var destroyCmd = &cobra.Command{
3232
Short: "destroy a lab",
3333
Long: "destroy a lab based defined by means of the topology definition file\nreference: https://containerlab.dev/cmd/destroy/",
3434
Aliases: []string{"des"},
35-
PreRunE: common.SudoCheck,
35+
PreRunE: common.CheckAndGetRootPrivs,
3636
RunE: destroyFn,
3737
}
3838

cmd/disableTxOffload.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
log "github.com/sirupsen/logrus"
1111
"github.com/spf13/cobra"
1212
"github.com/srl-labs/containerlab/clab"
13+
"github.com/srl-labs/containerlab/cmd/common"
1314
"github.com/srl-labs/containerlab/runtime"
1415
"github.com/srl-labs/containerlab/utils"
1516
)
@@ -21,6 +22,7 @@ var disableTxOffloadCmd = &cobra.Command{
2122
Use: "disable-tx-offload",
2223
Short: "disables tx checksum offload on eth0 interface of a container",
2324

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

cmd/exec.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/spf13/cobra"
1313
"github.com/srl-labs/containerlab/clab"
1414
"github.com/srl-labs/containerlab/clab/exec"
15-
"github.com/srl-labs/containerlab/cmd/common"
1615
"github.com/srl-labs/containerlab/labels"
1716
"github.com/srl-labs/containerlab/runtime"
1817
"github.com/srl-labs/containerlab/types"
@@ -26,10 +25,9 @@ var (
2625

2726
// execCmd represents the exec command.
2827
var execCmd = &cobra.Command{
29-
Use: "exec",
30-
Short: "execute a command on one or multiple containers",
31-
PreRunE: common.SudoCheck,
32-
RunE: execFn,
28+
Use: "exec",
29+
Short: "execute a command on one or multiple containers",
30+
RunE: execFn,
3331
}
3432

3533
func execFn(_ *cobra.Command, _ []string) error {
@@ -75,6 +73,11 @@ func execFn(_ *cobra.Command, _ []string) error {
7573
return err
7674
}
7775

76+
err = c.CheckConnectivity(ctx)
77+
if err != nil {
78+
return err
79+
}
80+
7881
var filters []*types.GenericFilter
7982

8083
if len(labelsFilter) != 0 {

cmd/generate.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
log "github.com/sirupsen/logrus"
1515
"github.com/spf13/cobra"
1616
"github.com/srl-labs/containerlab/clab"
17+
"github.com/srl-labs/containerlab/cmd/common"
1718
"github.com/srl-labs/containerlab/links"
1819
"github.com/srl-labs/containerlab/nodes"
1920
"github.com/srl-labs/containerlab/types"
@@ -89,6 +90,10 @@ var generateCmd = &cobra.Command{
8990
}
9091
}
9192
if deploy {
93+
err = common.CheckAndGetRootPrivs(nil, nil)
94+
if err != nil {
95+
return err
96+
}
9297
reconfigure = true
9398
if file == "" {
9499
file = fmt.Sprintf("%s.clab.yml", name)

cmd/inspect.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
log "github.com/sirupsen/logrus"
2020
"github.com/spf13/cobra"
2121
"github.com/srl-labs/containerlab/clab"
22-
"github.com/srl-labs/containerlab/cmd/common"
2322
"github.com/srl-labs/containerlab/labels"
2423
"github.com/srl-labs/containerlab/runtime"
2524
"github.com/srl-labs/containerlab/types"
@@ -38,7 +37,6 @@ var inspectCmd = &cobra.Command{
3837
Short: "inspect lab details",
3938
Long: "show details about a particular lab or all running labs\nreference: https://containerlab.dev/cmd/inspect/",
4039
Aliases: []string{"ins", "i"},
41-
PreRunE: common.SudoCheck,
4240
RunE: inspectFn,
4341
}
4442

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

86+
err = c.CheckConnectivity(ctx)
87+
if err != nil {
88+
return err
89+
}
90+
8891
var containers []runtime.GenericContainer
8992
var glabels []*types.GenericFilter
9093

cmd/redeploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var redeployCmd = &cobra.Command{
1313
Short: "destroy and redeploy a lab",
1414
Long: "destroy a lab and deploy it again based on the topology definition file\nreference: https://containerlab.dev/cmd/redeploy/",
1515
Aliases: []string{"rdep"},
16-
PreRunE: common.SudoCheck,
16+
PreRunE: common.CheckAndGetRootPrivs,
1717
SilenceUsage: true,
1818
RunE: redeployFn,
1919
}

cmd/root.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

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

93+
err := common.DropRootPrivs()
94+
if err != nil {
95+
return err
96+
}
97+
// Rootless operations only supported for Docker runtime
98+
if rt != "" && rt != "docker" {
99+
err := common.CheckAndGetRootPrivs(cmd, nil)
100+
if err != nil {
101+
return err
102+
}
103+
}
104+
92105
return getTopoFilePath(cmd)
93106
}
94107

cmd/save.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
log "github.com/sirupsen/logrus"
1313
"github.com/spf13/cobra"
1414
"github.com/srl-labs/containerlab/clab"
15-
"github.com/srl-labs/containerlab/cmd/common"
1615
"github.com/srl-labs/containerlab/links"
1716
"github.com/srl-labs/containerlab/nodes"
1817
"github.com/srl-labs/containerlab/runtime"
@@ -24,7 +23,6 @@ var saveCmd = &cobra.Command{
2423
Short: "save containers configuration",
2524
Long: `save performs a configuration save. The exact command that is used to save the config depends on the node kind.
2625
Refer to the https://containerlab.dev/cmd/save/ documentation to see the exact command used per node's kind`,
27-
PreRunE: common.SudoCheck,
2826
RunE: func(_ *cobra.Command, _ []string) error {
2927
if name == "" && topo == "" {
3028
return fmt.Errorf("provide topology file path with --topo flag")

0 commit comments

Comments
 (0)