-
Notifications
You must be signed in to change notification settings - Fork 602
agent/workloadattestor/docker: add rootless Podman support #6798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ import ( | |
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| "regexp" | ||
| "strconv" | ||
|
|
||
| "github.com/hashicorp/go-hclog" | ||
| "github.com/spiffe/spire/pkg/agent/common/cgroups" | ||
|
|
@@ -16,6 +18,16 @@ import ( | |
| "github.com/spiffe/spire/pkg/common/pluginconf" | ||
| ) | ||
|
|
||
| const ( | ||
| defaultPodmanSocketPath = "unix:///run/podman/podman.sock" | ||
| defaultPodmanSocketPathTemplate = "unix:///run/user/%d/podman/podman.sock" | ||
| ) | ||
|
|
||
| var ( | ||
| rePodmanCgroup = regexp.MustCompile(`(?:libpod-|/libpod/)`) | ||
| reUserSliceUID = regexp.MustCompile(`/user-(\d+)\.slice/`) | ||
| ) | ||
|
|
||
| type OSConfig struct { | ||
| // DockerSocketPath is the location of the docker daemon socket, this config can be used only on unix environments (default: "unix:///var/run/docker.sock"). | ||
| DockerSocketPath string `hcl:"docker_socket_path" json:"docker_socket_path"` | ||
|
|
@@ -33,6 +45,15 @@ type OSConfig struct { | |
| // about mountinfo and cgroup information used to locate the container. | ||
| VerboseContainerLocatorLogs bool `hcl:"verbose_container_locator_logs"` | ||
|
|
||
| // PodmanSocketPath is the socket path for rootful Podman (no user namespace). | ||
| // Defaults to "unix:///run/podman/podman.sock". | ||
| PodmanSocketPath string `hcl:"podman_socket_path" json:"podman_socket_path"` | ||
|
|
||
| // PodmanSocketPathTemplate is the socket path template for rootless Podman. | ||
| // The placeholder %d is replaced with the container owner's host UID extracted | ||
| // from the cgroup path. Defaults to "unix:///run/user/%d/podman/podman.sock". | ||
| PodmanSocketPathTemplate string `hcl:"podman_socket_path_template" json:"podman_socket_path_template"` | ||
|
|
||
| // Used by tests to use a fake /proc directory instead of the real one | ||
| rootDir string | ||
| } | ||
|
|
@@ -63,10 +84,25 @@ func (p *Plugin) createHelper(c *dockerPluginConfig, status *pluginconf.Status) | |
| rootDir = "/" | ||
| } | ||
|
|
||
| podmanSocketPath := c.PodmanSocketPath | ||
| if podmanSocketPath == "" { | ||
| podmanSocketPath = defaultPodmanSocketPath | ||
| } | ||
| podmanSocketPathTemplate := c.PodmanSocketPathTemplate | ||
| if podmanSocketPathTemplate == "" { | ||
| podmanSocketPathTemplate = defaultPodmanSocketPathTemplate | ||
| } | ||
| if err := validatePodmanSocketPathTemplate(podmanSocketPathTemplate); err != nil { | ||
| status.ReportErrorf("invalid podman_socket_path_template: %v", err) | ||
| return nil | ||
| } | ||
|
|
||
| return &containerHelper{ | ||
| rootDir: rootDir, | ||
| containerIDFinder: containerIDFinder, | ||
| verboseContainerLocatorLogs: c.VerboseContainerLocatorLogs, | ||
| podmanSocketPath: podmanSocketPath, | ||
| podmanSocketPathTemplate: podmanSocketPathTemplate, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -80,19 +116,76 @@ type containerHelper struct { | |
| rootDir string | ||
| containerIDFinder cgroup.ContainerIDFinder | ||
| verboseContainerLocatorLogs bool | ||
| podmanSocketPath string | ||
| podmanSocketPathTemplate string | ||
| } | ||
|
|
||
| func (h *containerHelper) getContainerID(pID int32, log hclog.Logger) (string, error) { | ||
| func (h *containerHelper) getContainerIDAndSocket(pID int32, log hclog.Logger) (string, string, error) { | ||
| if h.containerIDFinder != nil { | ||
| cgroupList, err := cgroups.GetCgroups(pID, dirFS(h.rootDir)) | ||
| if err != nil { | ||
| return "", err | ||
| return "", "", err | ||
| } | ||
| return getContainerIDFromCGroups(h.containerIDFinder, cgroupList) | ||
| containerID, err := getContainerIDFromCGroups(h.containerIDFinder, cgroupList) | ||
| if err != nil || containerID == "" { | ||
| return "", "", err | ||
| } | ||
| return containerID, h.detectPodmanSocket(cgroupList), nil | ||
| } | ||
|
|
||
| extractor := containerinfo.Extractor{RootDir: h.rootDir, VerboseLogging: h.verboseContainerLocatorLogs} | ||
| return extractor.GetContainerID(pID, log) | ||
| containerID, err := extractor.GetContainerID(pID, log) | ||
| if err != nil || containerID == "" { | ||
| return "", "", err | ||
| } | ||
|
|
||
| cgroupList, err := cgroups.GetCgroups(pID, dirFS(h.rootDir)) | ||
| if err != nil { | ||
| log.Warn("Failed to read cgroups for Podman detection, falling back to Docker client", "pid", pID, "err", err) | ||
| return containerID, "", nil | ||
| } | ||
| return containerID, h.detectPodmanSocket(cgroupList), nil | ||
| } | ||
|
|
||
| func (h *containerHelper) detectPodmanSocket(cgroupList []cgroups.Cgroup) string { | ||
| for _, cg := range cgroupList { | ||
| if !rePodmanCgroup.MatchString(cg.GroupPath) { | ||
| continue | ||
| } | ||
| if m := reUserSliceUID.FindStringSubmatch(cg.GroupPath); m != nil { | ||
| if uid, err := strconv.ParseUint(m[1], 10, 32); err == nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we should log a warning when ParseUint fails here. The regex matched a user slice, so the workload is rootless, but the code would silently fall through to the rootful Podman socket.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, thanks! Added a Warn log with the raw UID and cgroup path before falling back to the rootful socket. |
||
| return fmt.Sprintf(h.podmanSocketPathTemplate, uid) | ||
| } | ||
| } | ||
| return h.podmanSocketPath | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func validatePodmanSocketPathTemplate(template string) error { | ||
| var placeholders int | ||
| for i := 0; i < len(template); i++ { | ||
| if template[i] != '%' { | ||
| continue | ||
| } | ||
| if i+1 >= len(template) { | ||
| return errors.New("trailing % at end of template") | ||
| } | ||
| switch template[i+1] { | ||
| case '%': | ||
| i++ | ||
| case 'd': | ||
| placeholders++ | ||
| i++ | ||
| default: | ||
| return errors.New("template only supports escaped %% or the %d UID placeholder") | ||
| } | ||
| } | ||
|
|
||
| if placeholders != 1 { | ||
| return errors.New("template must contain exactly one %d UID placeholder") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func getDockerHost(c *dockerPluginConfig) string { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be cleaner to make the close obligation explicit at the type level, for example changing podmanClientFactory to return (Docker, io.Closer, error), rather than relying on a runtime type assertion in Attest.
I believe that would make the contract more discoverable. Looks like right now, to learn that the returned client should be closeable, you have to trace through Attest and find the type assertion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks! Replaced the runtime type assertion with a podmanDocker interface embedding Docker and Close() error, so podmanClientFactory now returns (podmanDocker, error) directly.