Skip to content

Commit 206932f

Browse files
committed
networks.Validate() requires that socket_vmnet is owned by root
This restriction was weakened by #1220 to only require the file and directories to be owned by the admin user, but that configuration is not secure. If users are willing to run an insecure configuration, then they can always enable password-less sudo, which does not need a sudoers file at all. Signed-off-by: Jan Dubois <[email protected]>
1 parent 713e1cd commit 206932f

File tree

7 files changed

+43
-59
lines changed

7 files changed

+43
-59
lines changed

cmd/limactl/sudoers.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ package main
33
import (
44
"errors"
55
"fmt"
6-
"os"
7-
"path/filepath"
86
"runtime"
97

108
"github.com/lima-vm/lima/pkg/networks"
9+
"github.com/sirupsen/logrus"
1110
"github.com/spf13/cobra"
1211
)
1312

13+
const networksURL = "https://lima-vm.io/docs/config/network/#socket_vmnet"
14+
1415
func newSudoersCommand() *cobra.Command {
15-
networksMD := "$PREFIX/share/doc/lima/docs/network.md"
16-
if exe, err := os.Executable(); err == nil {
17-
binDir := filepath.Dir(exe)
18-
prefixDir := filepath.Dir(binDir)
19-
networksMD = filepath.Join(prefixDir, "share/doc/lima/docs/network.md")
20-
}
2116
sudoersCommand := &cobra.Command{
2217
Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
2318
Example: `
@@ -31,7 +26,7 @@ $ limactl sudoers --check /etc/sudoers.d/lima
3126
Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support.
3227
The content is written to stdout, NOT to the file.
3328
This command must not run as the root user.
34-
See %s for the usage.`, networksMD),
29+
See %s for the usage.`, networksURL),
3530
Args: WrapArgsError(cobra.MaximumNArgs(1)),
3631
RunE: sudoersAction,
3732
GroupID: advancedCommand,
@@ -50,8 +45,12 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
5045
if err != nil {
5146
return err
5247
}
48+
if err := nwCfg.PasswordLessSudo(); err == nil {
49+
return errors.New("sudoers file is not needed because sudo doesn't require a password")
50+
}
5351
// Make sure the current network configuration is secure
5452
if err := nwCfg.Validate(); err != nil {
53+
logrus.Infof("Please check %s for more information.", networksURL)
5554
return err
5655
}
5756
check, err := cmd.Flags().GetBool("check")

examples/vmnet.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
# A template to enable vmnet.framework.
22

3-
# Usage:
4-
# brew install socket_vmnet
5-
# limactl sudoers >etc_sudoers.d_lima
6-
# sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
7-
# limactl start template://vmnet
3+
# Install socket_vmnet: https://lima-vm.io/docs/config/network/#socket_vmnet
84

95
# This template requires Lima v0.7.0 or later.
106
# Older versions of Lima were using a different syntax for supporting vmnet.framework.

pkg/networks/networks.TEMPLATE.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be
22
# installed where only root can modify/replace it. This means also none of the
3-
# parent directories should be writable by the user.
3+
# parent directories can be writable by the user.
44
#
55
# The varRun directory also must not be writable by the user because it will
66
# include the socket_vmnet pid file. Those will be terminated via sudo, so replacing

pkg/networks/sudoers.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func Sudoers() (string, error) {
5353
return sb.String(), nil
5454
}
5555

56-
func (c *Config) passwordLessSudo() error {
56+
func (c *Config) PasswordLessSudo() error {
5757
// Flush cached sudo password
5858
cmd := exec.Command("sudo", "-k")
5959
if err := cmd.Run(); err != nil {
@@ -81,12 +81,13 @@ func (c *Config) passwordLessSudo() error {
8181

8282
func (c *Config) VerifySudoAccess(sudoersFile string) error {
8383
if sudoersFile == "" {
84-
err := c.passwordLessSudo()
84+
err := c.PasswordLessSudo()
8585
if err == nil {
8686
logrus.Debug("sudo doesn't seem to require a password")
8787
return nil
8888
}
89-
return fmt.Errorf("passwordLessSudo error: %w", err)
89+
//nolint:revive // error-strings
90+
return fmt.Errorf("PasswordLessSudo error: %w", err)
9091
}
9192
hint := fmt.Sprintf("run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)",
9293
os.Args[0], sudoersFile)
@@ -95,12 +96,12 @@ func (c *Config) VerifySudoAccess(sudoersFile string) error {
9596
// Default networks.yaml specifies /etc/sudoers.d/lima file. Don't throw an error when the
9697
// file doesn't exist, as long as password-less sudo still works.
9798
if errors.Is(err, os.ErrNotExist) {
98-
err = c.passwordLessSudo()
99+
err = c.PasswordLessSudo()
99100
if err == nil {
100101
logrus.Debugf("%q does not exist, but sudo doesn't seem to require a password", sudoersFile)
101102
return nil
102103
}
103-
logrus.Debugf("%q does not exist; passwordLessSudo error: %s", sudoersFile, err)
104+
logrus.Debugf("%q does not exist; PasswordLessSudo error: %s", sudoersFile, err)
104105
}
105106
return fmt.Errorf("can't read %q: %w: (Hint: %s)", sudoersFile, err, hint)
106107
}

pkg/networks/validate.go

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import (
55
"fmt"
66
"io/fs"
77
"os"
8-
"os/user"
98
"path/filepath"
109
"reflect"
1110
"runtime"
12-
"strconv"
1311
"strings"
1412

1513
"github.com/lima-vm/lima/pkg/osutil"
@@ -100,49 +98,24 @@ func validatePath(path string, allowDaemonGroupWritable bool) error {
10098
if err != nil {
10199
return err
102100
}
103-
adminGroup, err := user.LookupGroup("admin")
104-
if err != nil {
105-
return err
106-
}
107-
adminGid, err := strconv.Atoi(adminGroup.Gid)
108-
if err != nil {
109-
return err
110-
}
111-
owner, err := user.LookupId(strconv.Itoa(int(stat.Uid)))
112-
if err != nil {
113-
return err
114-
}
115-
ownerIsAdmin := owner.Uid == "0"
116-
if !ownerIsAdmin {
117-
ownerGroupIDs, err := owner.GroupIds()
118-
if err != nil {
119-
return err
120-
}
121-
for _, g := range ownerGroupIDs {
122-
if g == adminGroup.Gid {
123-
ownerIsAdmin = true
124-
break
125-
}
126-
}
127-
}
128-
if !ownerIsAdmin {
129-
return fmt.Errorf(`%s %q owner %dis not an admin`, file, path, stat.Uid)
101+
if stat.Uid != root.Uid {
102+
return fmt.Errorf(`%s %q is not owned by %q (uid: %d), but by uid %d`, file, path, root.User, root.Uid, stat.Uid)
130103
}
131104
if allowDaemonGroupWritable {
132105
daemon, err := osutil.LookupUser("daemon")
133106
if err != nil {
134107
return err
135108
}
136-
if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) && stat.Gid != daemon.Gid {
137-
return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin, daemon]`,
138-
file, path, stat.Gid)
109+
if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != daemon.Gid {
110+
return fmt.Errorf(`%s %q is group-writable and group is neither %q (gid: %d) nor %q (gid: %d), but is gid: %d`,
111+
file, path, root.User, root.Gid, daemon.User, daemon.Gid, stat.Gid)
139112
}
140113
if fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0o010 == 0 || stat.Gid != daemon.Gid) {
141114
return fmt.Errorf(`%s %q is not executable by the %q (gid: %d)" group`, file, path, daemon.User, daemon.Gid)
142115
}
143-
} else if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) {
144-
return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin]`,
145-
file, path, stat.Gid)
116+
} else if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid {
117+
return fmt.Errorf(`%s %q is group-writable and group is not %q (gid: %d), but is gid: %d`,
118+
file, path, root.User, root.Gid, stat.Gid)
146119
}
147120
if fi.Mode()&0o02 != 0 {
148121
return fmt.Errorf("%s %q is world-writable", file, path)

pkg/store/filenames/filenames.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Package filenames defines the names of the files that appear under an instance dir
22
// or inside the config directory.
33
//
4-
// See docs/internal.md .
4+
// See https://lima-vm.io/docs/dev/internals/
55
package filenames
66

77
// Instance names starting with an underscore are reserved for lima internal usage

website/content/en/docs/config/network/_index.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,17 @@ The configuration steps are different for each network type:
6969
#### Managed (192.168.105.0/24)
7070

7171
[`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) is required for adding another guest IP that is accessible from the host and other guests.
72+
It must be installed according to the instruction provided on https://github.com/lima-vm/socket_vmnet.
73+
74+
Note that installation using Homebrew is not secure and not recommended by the Lima project.
75+
Homebrew installation will only work with Lima if password-less `sudo` is enabled for the current user.
76+
The `limactl sudoers` command requires that `socket_vmnet` is installed into a secure path only
77+
writable by `root` and will reject `socket_vmnet` installed by Homebrew into a user-writable location.
7278

7379
```bash
74-
# Install socket_vmnet
75-
brew install socket_vmnet
80+
# Install socket_vmnet via MacPorts or from source
81+
# using instructions on https://github.com/lima-vm/socket_vmnet
82+
7683

7784
# Set up the sudoers file for launching socket_vmnet from Lima
7885
limactl sudoers >etc_sudoers.d_lima
@@ -145,7 +152,7 @@ limactl start --network=lima:shared
145152
```yaml
146153
networks:
147154
# Lima can manage the socket_vmnet daemon for networks defined in $LIMA_HOME/_config/networks.yaml automatically.
148-
# The socket_vmnet binary must be installed into a secure location only alterable by the admin.
155+
# The socket_vmnet binary must be installed into a secure location only alterable by the "root" user.
149156
# - lima: shared
150157
# # MAC address of the instance; lima will pick one based on the instance name,
151158
# # so DHCP assigned ip addresses should remain constant over instance restarts.
@@ -160,6 +167,14 @@ The network daemon is started automatically when the first instance referencing
160167
and will stop automatically once the last instance has stopped. Daemon logs will be stored in the
161168
`$LIMA_HOME/_networks` directory.
162169

170+
Since the commands to start and stop the `socket_vmnet` daemon requires root, the user either must
171+
have password-less `sudo` enabled, or add the required commands to a `sudoers` file. This can
172+
be done via:
173+
174+
```shell
175+
limactl sudoers | sudo tee /etc/sudoers.d/lima
176+
```
177+
163178
The IP address is automatically assigned by macOS's bootpd.
164179
If the IP address is not assigned, try the following commands:
165180
```bash

0 commit comments

Comments
 (0)