Compare commits

...

7 Commits

Author SHA1 Message Date
Timmy Welch
e6c539bc29 Add Devices helper function 2026-01-17 21:27:20 -08:00
Timmy Welch
5d7a257b1f Fix socket path to default to docker socket path 2026-01-04 21:39:00 -08:00
Timmy Welch
94723719fe Fix passwd path 2026-01-04 21:13:59 -08:00
Timmy Welch
bee2165ddd Update setup 2026-01-04 20:55:25 -08:00
Timmy Welch
d050d58e92 Make Run and Start generic. An empty string defaults to "/user/podman" 2026-01-04 18:18:01 -08:00
Timmy Welch
fad0eb755a Add a podmanSocket command with hardcoded uid 199 2026-01-04 16:51:33 -08:00
Timmy Welch
587e906d6d Remove as many syscalls from new{g,u}idmap as possible
Add a test to validate subid generation
2026-01-04 16:23:01 -08:00
7 changed files with 193 additions and 41 deletions

56
main.go
View File

@@ -3,6 +3,7 @@ package Podman
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@@ -24,7 +25,11 @@ const (
G = 1024 * 1024 * 1024 G = 1024 * 1024 * 1024
) )
// Setup ensures that everything needed for running podman under the given uid is setup
// new{u,g}idmap is handled separately
// Note: This should be called while still root (before MustDropPrivileges) and must be called after any use of the GOKRAZY_FIRST_START environment variable
func Setup(uid int) error { func Setup(uid int) error {
// Needed so that calls to to podman which in turn call new{u,g}idmap will succeed
os.Unsetenv("GOKRAZY_FIRST_START") os.Unsetenv("GOKRAZY_FIRST_START")
err := os.Mkdir("/var/run/user", 0o755) err := os.Mkdir("/var/run/user", 0o755)
if err != nil && !errors.Is(err, os.ErrExist) { if err != nil && !errors.Is(err, os.ErrExist) {
@@ -45,6 +50,10 @@ func Setup(uid int) error {
return fmt.Errorf("unable to set owner for %s: %w", rundir, err) return fmt.Errorf("unable to set owner for %s: %w", rundir, err)
} }
} }
// catatonit needs /var/tmp for the pause container
if err := os.MkdirAll("/var/tmp", 0o777); err != nil {
fmt.Errorf("unable to create '/var/tmp': %w", err)
}
err = os.Chmod("/var/tmp", 0o1777) err = os.Chmod("/var/tmp", 0o1777)
if err != nil { if err != nil {
return fmt.Errorf("unable to set perms for /var/tmp: %w", err) return fmt.Errorf("unable to set perms for /var/tmp: %w", err)
@@ -65,11 +74,34 @@ func Setup(uid int) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to set perms for /dev/net/tun: %w", err) return fmt.Errorf("unable to set perms for /dev/net/tun: %w", err)
} }
if err := MakeWritable("/etc/containers/networks/"); err != nil {
return fmt.Errorf("unable to make writeable for /etc/containers/networks/: %w", err)
}
if err := os.MkdirAll("/var/lib/containers/storage/volumes", 0o777); err != nil {
return fmt.Errorf("unable to create /var/lib/containers/storage/volumes: %w", err)
}
// podman default socket location is /run/podman/podman.sock
if err := os.MkdirAll("/run/podman/", 0o770); err != nil {
return fmt.Errorf("unable to create /run/podman/: %w", err)
}
if err := os.Symlink("/proc/self/fd/0", "/dev/stdin"); err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("unable to make /dev/stdin symlink: %w", err)
}
if err := os.Symlink("/proc/self/fd/1", "/dev/stdout"); err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("unable to make /dev/stdout symlink: %w", err)
}
if err := os.Symlink("/proc/self/fd/2", "/dev/stderr"); err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("unable to make /dev/stderr symlink: %w", err)
}
return nil return nil
} }
func Run(args ...string) error { func Run(command string, args ...string) error {
podman := exec.Command("/user/podman", args...) if command == "" {
command = "/user/podman"
}
podman := exec.Command(command, args...)
podman.Env = append(os.Environ(), "TMPDIR=/tmp") podman.Env = append(os.Environ(), "TMPDIR=/tmp")
podman.Stdin = os.Stdin podman.Stdin = os.Stdin
podman.Stdout = os.Stdout podman.Stdout = os.Stdout
@@ -91,8 +123,11 @@ func Run(args ...string) error {
return nil return nil
} }
func Start(args ...string) (chan os.Signal, *exec.Cmd, error) { func Start(command string, args ...string) (chan os.Signal, *exec.Cmd, error) {
podman := exec.Command("/user/podman", args...) if command == "" {
command = "/user/podman"
}
podman := exec.Command(command, args...)
podman.Env = append(os.Environ(), "TMPDIR=/tmp") podman.Env = append(os.Environ(), "TMPDIR=/tmp")
podman.Stdin = os.Stdin podman.Stdin = os.Stdin
podman.Stdout = os.Stdout podman.Stdout = os.Stdout
@@ -113,6 +148,17 @@ func Start(args ...string) (chan os.Signal, *exec.Cmd, error) {
return exit, podman, nil return exit, podman, nil
} }
func Devices(devices ...string) []string {
devs := []string{}
for _, dev := range devices {
_, err := os.Stat("/dev/" + dev)
if err == nil {
devs = append(devs, "--device=/dev/"+dev)
}
}
return devs
}
func BindRO(src, dst string) string { func BindRO(src, dst string) string {
return fmt.Sprintf(`--mount=type=bind,source=%s,destination=%s,ro=true`, src, dst) return fmt.Sprintf(`--mount=type=bind,source=%s,destination=%s,ro=true`, src, dst)
} }
@@ -174,7 +220,7 @@ func IsMounted(mountpoint string) (bool, error) {
return false, err return false, err
} }
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") { for line := range strings.SplitSeq(strings.TrimSpace(string(b)), "\n") {
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) < 5 { if len(parts) < 5 {
continue continue

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"os" "os"
"gitea.narnian.us/lordwelch/Podman" "gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() { func main() {
if os.Getenv("GOKRAZY_FIRST_START") == "1" { if os.Getenv("GOKRAZY_FIRST_START") == "1" {
Podman.WriteSubids("/etc/subgid") subidContent, err := Podman.GetSubids("/etc/passwd")
if err != nil {
log.Printf("Unable to generate /etc/subgid successfully, podman will probably not work: %s", err)
}
err = os.WriteFile("/etc/subgid", subidContent, 0o644)
if err != nil {
log.Printf("Unable to write to /etc/subgid, podman will probably not work: %s", err)
}
os.Exit(125) os.Exit(125)
} }
Podman.NewIDMap("gid_map") Podman.NewIDMap("gid_map")

View File

@@ -7,6 +7,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -56,34 +57,39 @@ func NewIDMap(idMapName string) {
log.Fatal("You aren't allowed to do that :-P") log.Fatal("You aren't allowed to do that :-P")
} }
var ( var (
user Passwd // This will get overwritten by /etc/passwd if we read subids from files
subid moby_user.SubID user = Passwd{
passwds []Passwd = LoadAllPasswd() User: os.Getenv("USER"),
UID: int(fstat.Uid),
GID: int(fstat.Gid),
}
subid = generateSubid(int64(fstat.Uid))
) )
subidstart := subidStart subid.Name = user.User
for _, u := range passwds {
if u.UID == int(fstat.Uid) { if subidUseFile {
user = u passwds, err := LoadAllPasswd()
subid = moby_user.SubID{ if err != nil {
Name: user.User, // Continue because we don't actually want to fail if there is no /etc/passwd
SubID: subidstart, // This command assumes a static mapping of subids based on the current uid. Which is already verified above
Count: subidCount, log.Println(err)
}
for _, u := range passwds {
if u.UID == int(fstat.Uid) {
user = u
} }
} }
subidstart += subidCount if user.User == "" {
} log.Fatalf("Unable to find user")
if user.User == "" { }
log.Fatalf("Unable to find user")
}
if subidUseFile {
if idMapName == "uid_map" { if idMapName == "uid_map" {
subid, err = getSubIDs("/etc/subuid", user.User, strconv.Itoa(user.UID)) subid, err = getSubIDs("/etc/subuid", user.User, strconv.Itoa(user.UID))
} else { } else {
subid, err = getSubIDs("/etc/subgid", user.User, strconv.Itoa(user.GID)) subid, err = getSubIDs("/etc/subgid", user.User, strconv.Itoa(user.GID))
} }
} if err != nil {
if err != nil { log.Fatalf("Unable to get subid map for the user: %v", err)
log.Fatalf("Unable to get subid map for the user: %v", err) }
} }
idmaps, err := getIDMaps(int64(user.UID), subid, args) idmaps, err := getIDMaps(int64(user.UID), subid, args)
if err != nil { if err != nil {
@@ -118,19 +124,28 @@ func NewIDMap(idMapName string) {
} }
} }
func WriteSubids(file string) error { func GetSubids(passwd string) ([]byte, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
passwds := LoadAllPasswd() passwds, err := LoadAllPasswd(passwd)
subidstart := subidStart if err != nil { // Only errors are io errors reading /etc/passwd
return nil, err
}
sort.Slice(passwds, func(i, j int) bool { return passwds[i].UID < passwds[j].UID })
for _, passwd := range passwds { for _, passwd := range passwds {
fmt.Fprintf(buf, "%s:%d:%d\n", passwd.User, subidstart, subidCount) subid := generateSubid(int64(passwd.UID))
subidstart += subidCount _, err := fmt.Fprintf(buf, "%s:%d:%d\n", passwd.User, subid.SubID, subid.Count)
if err != nil {
return nil, err
}
} }
err := os.WriteFile(file, buf.Bytes(), 0o644) return buf.Bytes(), nil
if err != nil { }
return err
func generateSubid(id int64) moby_user.SubID {
return moby_user.SubID{
SubID: subidStart + (subidCount * id),
Count: subidCount,
} }
return nil
} }
func getIDMaps(id int64, subid moby_user.SubID, args []string) ([]moby_user.IDMap, error) { func getIDMaps(id int64, subid moby_user.SubID, args []string) ([]moby_user.IDMap, error) {

38
newidmap_test.go Normal file
View File

@@ -0,0 +1,38 @@
package Podman_test
import (
"os"
"testing"
"gitea.narnian.us/lordwelch/Podman"
)
// passwdContent is specifically not in order
const passwdContent = `root:x:0:0:root:/perm/home:/perm/bin/bash
vw:x:2:2:vw:/perm/home/vw:/bin/nologin
jellyfin:x:1:1:jellyfin:/perm/home/jellyfin:/bin/nologin
nzbget:x:3:3:nzbget:/perm/home/nzbget:/bin/nologin
`
// expected is always in uid order
const expected = `root:200:65536
jellyfin:65736:65536
vw:131272:65536
nzbget:196808:65536
`
func TestGetSubids(t *testing.T) {
tmpDir := t.TempDir()
passwd := tmpDir + "/passwd"
err := os.WriteFile(passwd, []byte(passwdContent), 0o644)
if err != nil {
t.Fatalf("Unable to write %s: %s", passwd, err)
}
ids, err := Podman.GetSubids(passwd)
if err != nil {
t.Fatal("Unable to generate subids", err)
}
if got, want := string(ids), expected; got != want {
t.Errorf("GetSubids: unexpected result: got %q, want %q", got, want)
}
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"os" "os"
"gitea.narnian.us/lordwelch/Podman" "gitea.narnian.us/lordwelch/Podman"
@@ -8,7 +9,14 @@ import (
func main() { func main() {
if os.Getenv("GOKRAZY_FIRST_START") == "1" { if os.Getenv("GOKRAZY_FIRST_START") == "1" {
Podman.WriteSubids("/etc/subuid") subidContent, err := Podman.GetSubids("/etc/passwd")
if err != nil {
log.Printf("Unable to generate /etc/subuid successfully, podman will probably not work: %s", err)
}
err = os.WriteFile("/etc/subuid", subidContent, 0o644)
if err != nil {
log.Printf("Unable to write to /etc/subuid, podman will probably not work: %s", err)
}
os.Exit(125) os.Exit(125)
} }
Podman.NewIDMap("uid_map") Podman.NewIDMap("uid_map")

28
podmanSocket/main.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"log"
"os"
"syscall"
"gitea.narnian.us/lordwelch/Podman"
)
func main() {
Podman.Setup(199) // subuids start at 200
Podman.MustDropPrivileges(Podman.Passwd{
User: "podmanSocket",
UID: 199,
GID: 199,
Home: "/perm/home/podmanSocket",
Shell: "/bin/sh",
})
args := []string{
"/user/podman", "system", "--log-level=debug", "service", "--time=0", "unix:///run/docker.sock",
}
err := syscall.Exec("/user/podman", args, os.Environ())
if err != nil {
log.Fatalf("failed to start podman: %v", err)
}
}

View File

@@ -1,6 +1,7 @@
package Podman package Podman
import ( import (
"fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -70,8 +71,9 @@ type Passwd struct {
func MustDropPrivileges(passwd Passwd, caps ...string) { func MustDropPrivileges(passwd Passwd, caps ...string) {
err := os.Chown(passwd.Home, passwd.UID, passwd.GID) err := os.Chown(passwd.Home, passwd.UID, passwd.GID)
if err != nil { if err != nil {
log.Print("Failed to chown home directory", os.Getenv("HOME")) log.Printf("Failed to chown home directory %s", passwd.Home)
} }
os.Setenv("HOME", passwd.Home)
os.Setenv("USER", passwd.User) os.Setenv("USER", passwd.User)
c := cap.GetProc() c := cap.GetProc()
if os.Getenv("PRIVILEGES_DROPPED") == "1" { if os.Getenv("PRIVILEGES_DROPPED") == "1" {
@@ -141,11 +143,18 @@ func LoadPasswd() Passwd {
} }
return passwd return passwd
} }
func LoadAllPasswd() []Passwd {
// LoadAllPasswd reads /etc/passwd by default.
// If a string is passed the first argument is used as the path instead of /etc/passwd
func LoadAllPasswd(passwd ...string) ([]Passwd, error) {
var passwds []Passwd var passwds []Passwd
b, err := os.ReadFile("/etc/passwd") var passwdFile = "/etc/passwd"
if len(passwd) > 0 {
passwdFile = passwd[0]
}
b, err := os.ReadFile(passwdFile)
if err != nil { if err != nil {
return passwds return passwds, fmt.Errorf("Unable to read /etc/passwd: %w", err)
} }
for line := range strings.SplitSeq(string(b), "\n") { for line := range strings.SplitSeq(string(b), "\n") {
fields := strings.SplitN(line, ":", 7) fields := strings.SplitN(line, ":", 7)
@@ -168,5 +177,5 @@ func LoadAllPasswd() []Passwd {
passwd.Shell = fields[6] passwd.Shell = fields[6]
passwds = append(passwds, passwd) passwds = append(passwds, passwd)
} }
return passwds return passwds, nil
} }