// Podman runtime adapter for Sorcery-Go Sanctums. // // Uses the Podman CLI (podman create, podman start, podman stop, etc.) // to manage OCI containers. Podman provides rootless containers, // OCI-compliant images, and is compatible with Docker workflows // while being daemonless and more secure by default. // // Podman containers use Linux namespaces + cgroups for isolation, // same as LXC, but are managed through the OCI runtime (crun/runc). // The eBPF enforcer attaches at the cgroup level, so it works // identically for both LXC and Podman. package runtime import ( "context" "encoding/json" "fmt" "os" "os/exec" "strconv" "strings" "time" ) // PodmanRuntime manages Podman OCI containers. type PodmanRuntime struct { binPath string rootless bool } // NewPodmanRuntime creates a Podman runtime adapter. func NewPodmanRuntime() *PodmanRuntime { return &PodmanRuntime{ binPath: "podman", } } func (r *PodmanRuntime) Type() Type { return RuntimePodman } func (r *PodmanRuntime) Name() string { return "Podman OCI containers" } // Probe checks that podman is available. func (r *PodmanRuntime) Probe() error { cmd := exec.Command("podman", "--version") out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: not found in PATH: %w\n%s", err, string(out)) } // Check if running rootless (version string contains "rootless"). version := strings.TrimSpace(string(out)) r.rootless = strings.Contains(version, "rootless") return nil } // Create provisions a new Podman container. func (r *PodmanRuntime) Create(ctx context.Context, opts CreateOpts) (string, error) { args := []string{"create", "--name", opts.Name} if opts.Image != "" { args = append(args, opts.Image) } // Network configuration. if opts.NetworkConfig != nil { switch opts.NetworkConfig.Type { case "none": args = append(args, "--network", "none") case "host": args = append(args, "--network", "host") case "bridge": if opts.NetworkConfig.Bridge != "" { args = append(args, "--network", opts.NetworkConfig.Bridge) } if opts.NetworkConfig.IP != "" { args = append(args, "--ip", opts.NetworkConfig.IP) } if opts.NetworkConfig.MACAddress != "" { args = append(args, "--mac-address", opts.NetworkConfig.MACAddress) } } } // Bind mounts. for _, m := range opts.BindMounts { args = append(args, "--mount", "type=bind,source="+m.HostPath+",destination="+m.ContainerPath+",readonly="+strconv.FormatBool(m.ReadOnly)) } // Environment variables. for k, v := range opts.EnvVars { args = append(args, "-e", k+"="+v) } // Resource limits. if opts.MemoryMB > 0 { args = append(args, "--memory", fmt.Sprintf("%dm", opts.MemoryMB)) } // Capabilities: drop all, then add back if specified. args = append(args, "--cap-drop", "ALL") for _, cap := range opts.Caps { args = append(args, "--cap-add", cap) } // SECURITY: Disable default AppArmor and seccomp confinement. // This is intentional — the sorcery-go eBPF Tomb Guard LSM handles // mandatory access control (MAC) enforcement, making the container-level // profiles redundant. Re-enabling them would conflict with the eBPF // enforcer's cgroup-level hook attachments. args = append(args, "--security-opt", "apparmor=unconfined") args = append(args, "--security-opt", "seccomp=unconfined") // Architecture. if opts.Arch != "" { args = append(args, "--arch", opts.Arch) } // Extra podman-specific options. for k, v := range opts.ExtraConfig { args = append(args, k, v) } // If no image was specified and RootFS is provided, use a scratch image. if opts.Image == "" && opts.RootFS != "" { args = append(args, "scratch") } cmd := exec.CommandContext(ctx, "podman", args...) out, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("podman: create %s: %w\n%s", opts.Name, err, string(out)) } // Parse container ID from output (podman create prints the ID). containerID := strings.TrimSpace(string(out)) if containerID == "" { containerID = opts.Name } return containerID, nil } // Start starts a Podman container. func (r *PodmanRuntime) Start(ctx context.Context, sanctumID string) error { cmd := exec.CommandContext(ctx, "podman", "start", sanctumID) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: start %s: %w\n%s", sanctumID, err, string(out)) } return nil } // Stop stops a Podman container. func (r *PodmanRuntime) Stop(ctx context.Context, sanctumID string) error { cmd := exec.CommandContext(ctx, "podman", "stop", "-t", "30", sanctumID) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: stop %s: %w\n%s", sanctumID, err, string(out)) } return nil } // Freeze suspends a Podman container via cgroup v2 freezer. func (r *PodmanRuntime) Freeze(ctx context.Context, sanctumID string) error { // Podman containers can be frozen via cgroups or podman pause. cgPath := r.CgroupPath(sanctumID) if cgPath != "" { freezeFile := cgPath + "/cgroup.freeze" if _, err := os.Stat(freezeFile); err == nil { if err := os.WriteFile(freezeFile, []byte("1"), 0644); err == nil { return nil } } } // Fallback: podman pause. cmd := exec.CommandContext(ctx, "podman", "pause", sanctumID) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: freeze %s: %w\n%s", sanctumID, err, string(out)) } return nil } // Thaw resumes a paused Podman container. func (r *PodmanRuntime) Thaw(ctx context.Context, sanctumID string) error { cgPath := r.CgroupPath(sanctumID) if cgPath != "" { freezeFile := cgPath + "/cgroup.freeze" if _, err := os.Stat(freezeFile); err == nil { if err := os.WriteFile(freezeFile, []byte("0"), 0644); err == nil { return nil } } } // Fallback: podman unpause. cmd := exec.CommandContext(ctx, "podman", "unpause", sanctumID) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: thaw %s: %w\n%s", sanctumID, err, string(out)) } return nil } // Destroy removes a Podman container. func (r *PodmanRuntime) Destroy(ctx context.Context, sanctumID string) error { // Force remove (handles running containers too). cmd := exec.CommandContext(ctx, "podman", "rm", "-f", sanctumID) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("podman: destroy %s: %w\n%s", sanctumID, err, string(out)) } return nil } // Exec runs a command inside a Podman container. func (r *PodmanRuntime) Exec(ctx context.Context, sanctumID string, command []string, stdin []byte) (*ExecResult, error) { args := append([]string{"exec", sanctumID}, command...) cmd := exec.CommandContext(ctx, "podman", args...) if len(stdin) > 0 { cmd.Stdin = strings.NewReader(string(stdin)) } var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() return &ExecResult{ ExitCode: exitCode(err), Stdout: []byte(stdout.String()), Stderr: []byte(stderr.String()), }, err } // Status returns the current state of a Podman container. func (r *PodmanRuntime) Status(ctx context.Context, sanctumID string) (*SanctumInfo, error) { cmd := exec.CommandContext(ctx, "podman", "inspect", sanctumID, "--format", "json") out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("podman: inspect %s: %w", sanctumID, err) } var containers []struct { ID string `json:"Id"` Name string `json:"Name"` State string `json:"State"` Status string `json:"Status"` PID int `json:"Pid"` IPAddress string `json:"IPAddress"` Created string `json:"Created"` Config struct { Labels map[string]string `json:"Labels"` } `json:"Config"` } if err := json.Unmarshal(out, &containers); err != nil || len(containers) == 0 { return nil, fmt.Errorf("podman: failed to parse inspect output for %s", sanctumID) } c := containers[0] created, _ := time.Parse(time.RFC3339Nano, c.Created) return &SanctumInfo{ ID: c.ID[:12], Name: c.Name, Runtime: RuntimePodman, Status: parsePodmanState(c.State), Arch: "", IP: c.IPAddress, PID: uint32(c.PID), Cgroup: r.CgroupPath(sanctumID), Created: created, Metadata: c.Config.Labels, }, nil } // List returns all Podman containers. func (r *PodmanRuntime) List(ctx context.Context) ([]*SanctumInfo, error) { cmd := exec.CommandContext(ctx, "podman", "ps", "-a", "--format", "json") out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("podman: list: %w", err) } var containers []struct { ID string `json:"Id"` Name string `json:"Names"` State string `json:"State"` Image string `json:"Image"` PID int `json:"Pid"` IPAddress string `json:"IPAddress"` Created int64 `json:"CreatedAt"` } if err := json.Unmarshal(out, &containers); err != nil { return nil, fmt.Errorf("podman: failed to parse ps output: %w", err) } var infos []*SanctumInfo for _, c := range containers { names := strings.Split(c.Name, ",") name := names[0] infos = append(infos, &SanctumInfo{ ID: c.ID[:12], Name: name, Runtime: RuntimePodman, Status: parsePodmanState(c.State), IP: c.IPAddress, PID: uint32(c.PID), Cgroup: r.CgroupPath(c.ID[:12]), Created: time.Unix(c.Created, 0), }) } return infos, nil } // CgroupPath returns the cgroup v2 path for a Podman container. // Podman places containers under the systemd scope hierarchy. func (r *PodmanRuntime) CgroupPath(sanctumID string) string { // Podman uses libpod-cni or libpod-podman scopes. // Try common cgroup v2 paths for podman containers. candidates := []string{ // Rootless podman "/sys/fs/cgroup/user.slice/user-" + strconv.Itoa(os.Getuid()) + ".slice/user@" + strconv.Itoa(os.Getuid()) + ".service/" + "libpod-" + sanctumID + ".scope", // Rootful podman "/sys/fs/cgroup/machine.slice/libpod-" + sanctumID + ".scope", "/sys/fs/cgroup/system.slice/libpod-" + sanctumID + ".scope", // Podman with cgroupns=host "/sys/fs/cgroup/libpod_parent/libpod-" + sanctumID + ".scope", } for _, p := range candidates { if _, err := statPath(p + "/cgroup.freeze"); err == nil { return p } } // Try to find by container name (podman uses container name in some paths). nameCandidates := []string{ "/sys/fs/cgroup/machine.slice/podman-" + sanctumID + ".scope", "/sys/fs/cgroup/machine.slice/libpod_parent-" + sanctumID + ".scope", } for _, p := range nameCandidates { if _, err := statPath(p + "/cgroup.freeze"); err == nil { return p } } // Try PID-based lookup. return cgroupV2PathByPID(sanctumID) } // podmanStates maps lowercase Podman state strings to SanctumStatus. // Falls through to StatusUnknown for unrecognized states. var podmanStates = map[string]SanctumStatus{ "running": StatusRunning, "stopped": StatusStopped, "exited": StatusStopped, "dead": StatusStopped, "paused": StatusFrozen, "created": StatusCreating, } func parsePodmanState(s string) SanctumStatus { if st, ok := podmanStates[strings.ToLower(s)]; ok { return st } return StatusUnknown } // statPath wraps os.Stat for use in cgroup path detection. func statPath(p string) (os.FileInfo, error) { return os.Stat(p) }