Skip to content

Commit f71dfcb

Browse files
n1hilitymheon
authored andcommitted
Initial implementation of mac forwarding using a privileged docker sock claim helper
Signed-off-by: Jason T. Greene <[email protected]>
1 parent 2128236 commit f71dfcb

File tree

8 files changed

+787
-9
lines changed

8 files changed

+787
-9
lines changed

Makefile

+11-1
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,23 @@ podman-winpath: .gopathok $(SOURCES) go.mod go.sum
376376
./cmd/winpath
377377

378378
.PHONY: podman-remote-darwin
379-
podman-remote-darwin: ## Build podman-remote for macOS
379+
podman-remote-darwin: podman-mac-helper ## Build podman-remote for macOS
380380
$(MAKE) \
381381
CGO_ENABLED=$(DARWIN_GCO) \
382382
GOOS=darwin \
383383
GOARCH=$(GOARCH) \
384384
bin/darwin/podman
385385

386+
.PHONY: podman-mac-helper
387+
podman-mac-helper: ## Build podman-mac-helper for macOS
388+
CGO_ENABLED=0 \
389+
GOOS=darwin \
390+
GOARCH=$(GOARCH) \
391+
$(GO) build \
392+
$(BUILDFLAGS) \
393+
-o bin/darwin/podman-mac-helper \
394+
./cmd/podman-mac-helper
395+
386396
bin/rootlessport: .gopathok $(SOURCES) go.mod go.sum
387397
CGO_ENABLED=$(CGO_ENABLED) \
388398
$(GO) build \

cmd/podman-mac-helper/install.go

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//go:build darwin
2+
// +build darwin
3+
4+
package main
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"io"
10+
"io/fs"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
"syscall"
15+
"text/template"
16+
17+
"github.com/pkg/errors"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
const (
22+
rwx_rx_rx = 0755
23+
rw_r_r = 0644
24+
)
25+
26+
const launchConfig = `<?xml version="1.0" encoding="UTF-8"?>
27+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28+
<plist version="1.0">
29+
<dict>
30+
<key>Label</key>
31+
<string>com.github.containers.podman.helper-{{.User}}</string>
32+
<key>ProgramArguments</key>
33+
<array>
34+
<string>{{.Program}}</string>
35+
<string>service</string>
36+
<string>{{.Target}}</string>
37+
</array>
38+
<key>inetdCompatibility</key>
39+
<dict>
40+
<key>Wait</key>
41+
<false/>
42+
</dict>
43+
<key>UserName</key>
44+
<string>root</string>
45+
<key>Sockets</key>
46+
<dict>
47+
<key>Listeners</key>
48+
<dict>
49+
<key>SockFamily</key>
50+
<string>Unix</string>
51+
<key>SockPathName</key>
52+
<string>/private/var/run/podman-helper-{{.User}}.socket</string>
53+
<key>SockPathOwner</key>
54+
<integer>{{.UID}}</integer>
55+
<key>SockPathMode</key>
56+
<!-- SockPathMode takes base 10 (384 = 0600) -->
57+
<integer>384</integer>
58+
<key>SockType</key>
59+
<string>stream</string>
60+
</dict>
61+
</dict>
62+
</dict>
63+
</plist>
64+
`
65+
66+
type launchParams struct {
67+
Program string
68+
User string
69+
UID string
70+
Target string
71+
}
72+
73+
var installCmd = &cobra.Command{
74+
Use: "install",
75+
Short: "installs the podman helper agent",
76+
Long: "installs the podman helper agent, which manages the /var/run/docker.sock link",
77+
PreRun: silentUsage,
78+
RunE: install,
79+
}
80+
81+
func init() {
82+
addPrefixFlag(installCmd)
83+
rootCmd.AddCommand(installCmd)
84+
}
85+
86+
func install(cmd *cobra.Command, args []string) error {
87+
userName, uid, homeDir, err := getUser()
88+
if err != nil {
89+
return err
90+
}
91+
92+
labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName)
93+
fileName := filepath.Join("/Library", "LaunchDaemons", labelName)
94+
95+
if _, err := os.Stat(fileName); err == nil || !os.IsNotExist(err) {
96+
return errors.New("helper is already installed, uninstall first")
97+
}
98+
99+
prog, err := installExecutable(userName)
100+
if err != nil {
101+
return err
102+
}
103+
104+
target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
105+
var buf bytes.Buffer
106+
t := template.Must(template.New("launchdConfig").Parse(launchConfig))
107+
err = t.Execute(&buf, launchParams{prog, userName, uid, target})
108+
if err != nil {
109+
return err
110+
}
111+
112+
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, rw_r_r)
113+
if err != nil {
114+
return errors.Wrap(err, "error creating helper plist file")
115+
}
116+
defer file.Close()
117+
_, err = buf.WriteTo(file)
118+
if err != nil {
119+
return err
120+
}
121+
122+
if err = runDetectErr("launchctl", "load", fileName); err != nil {
123+
return errors.Wrap(err, "launchctl failed loading service")
124+
}
125+
126+
return nil
127+
}
128+
129+
func restrictRecursive(targetDir string, until string) error {
130+
for targetDir != until && len(targetDir) > 1 {
131+
info, err := os.Lstat(targetDir)
132+
if err != nil {
133+
return err
134+
}
135+
if info.Mode()&fs.ModeSymlink != 0 {
136+
return errors.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir)
137+
}
138+
if err = os.Chown(targetDir, 0, 0); err != nil {
139+
return errors.Wrap(err, "could not update ownership of helper path")
140+
}
141+
if err = os.Chmod(targetDir, rwx_rx_rx|fs.ModeSticky); err != nil {
142+
return errors.Wrap(err, "could not update permissions of helper path")
143+
}
144+
targetDir = filepath.Dir(targetDir)
145+
}
146+
147+
return nil
148+
}
149+
150+
func verifyRootDeep(path string) error {
151+
path = filepath.Clean(path)
152+
current := "/"
153+
segs := strings.Split(path, "/")
154+
depth := 0
155+
for i := 1; i < len(segs); i++ {
156+
seg := segs[i]
157+
current = filepath.Join(current, seg)
158+
info, err := os.Lstat(current)
159+
if err != nil {
160+
return err
161+
}
162+
163+
stat := info.Sys().(*syscall.Stat_t)
164+
if stat.Uid != 0 {
165+
return errors.Errorf("installation target path must be solely owned by root: %s is not", current)
166+
}
167+
168+
if info.Mode()&fs.ModeSymlink != 0 {
169+
target, err := os.Readlink(current)
170+
if err != nil {
171+
return err
172+
}
173+
174+
targetParts := strings.Split(target, "/")
175+
segs = append(targetParts, segs[i+1:]...)
176+
177+
if depth++; depth > 1000 {
178+
return errors.New("reached max recursion depth, link structure is cyclical or too complex")
179+
}
180+
181+
if !filepath.IsAbs(target) {
182+
current = filepath.Dir(current)
183+
i = -1 // Start at 0
184+
} else {
185+
current = "/"
186+
i = 0 // Skip empty first segment
187+
}
188+
}
189+
}
190+
191+
return nil
192+
}
193+
194+
func installExecutable(user string) (string, error) {
195+
// Since the installed executable runs as root, as a precaution verify root ownership of
196+
// the entire installation path, and utilize sticky + read only perms for the helper path
197+
// suffix. The goal is to help users harden against privilege escalation from loose
198+
// filesystem permissions.
199+
//
200+
// Since userpsace package management tools, such as brew, delegate management of system
201+
// paths to standard unix users, the daemon executable is copied into a separate more
202+
// restricted area of the filesystem.
203+
if err := verifyRootDeep(installPrefix); err != nil {
204+
return "", err
205+
}
206+
207+
targetDir := filepath.Join(installPrefix, "podman", "helper", user)
208+
if err := os.MkdirAll(targetDir, rwx_rx_rx); err != nil {
209+
return "", errors.Wrap(err, "could not create helper directory structure")
210+
}
211+
212+
// Correct any incorrect perms on previously existing directories and verify no symlinks
213+
if err := restrictRecursive(targetDir, installPrefix); err != nil {
214+
return "", err
215+
}
216+
217+
exec, err := os.Executable()
218+
if err != nil {
219+
return "", err
220+
}
221+
install := filepath.Join(targetDir, filepath.Base(exec))
222+
223+
return install, copyFile(install, exec, rwx_rx_rx)
224+
}
225+
226+
func copyFile(dest string, source string, perms fs.FileMode) error {
227+
in, err := os.Open(source)
228+
if err != nil {
229+
return err
230+
}
231+
232+
defer in.Close()
233+
out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
234+
if err != nil {
235+
return err
236+
}
237+
238+
defer out.Close()
239+
if _, err := io.Copy(out, in); err != nil {
240+
return err
241+
}
242+
243+
return nil
244+
}

0 commit comments

Comments
 (0)