// pkg-helper is a root-privileged helper that listens on a Unix socket // and executes apk add/del commands on behalf of the non-root app process. // It is started by docker-entrypoint.sh before dropping privileges. package main import ( "bufio" "encoding/json" "fmt" "log/slog" "net" "os" "os/exec" "os/signal" "regexp" "strings" "syscall" "time" ) const socketPath = "/tmp/pkg.sock" // validPkgName allows alphanumeric, hyphens, underscores, dots, @, / (scoped npm). // Rejects names starting with - to prevent argument injection. var validPkgName = regexp.MustCompile(`^[a-zA-Z0-9@][a-zA-Z0-9._+\-/@]*$`) type request struct { Action string `json:"action"` Package string `json:"package"` } type response struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` } func main() { slog.Info("pkg-helper: starting", "socket", socketPath) // Remove stale socket. os.Remove(socketPath) // Restrictive umask: socket created as 0660 (not default 0777). oldMask := syscall.Umask(0117) listener, err := net.Listen("unix", socketPath) syscall.Umask(oldMask) if err != nil { slog.Error("pkg-helper: listen failed", "error", err) os.Exit(1) } defer listener.Close() // Socket permissions: owner root, group goclaw (gid 1000), mode 0660. // Chown requires CAP_CHOWN; if missing (misconfigured container), warn but continue // since umask already set restrictive permissions. if os.Getuid() == 0 { if err := os.Chown(socketPath, 0, 1000); err != nil { slog.Warn("pkg-helper: chown socket failed (missing CAP_CHOWN?)", "error", err) } } if err := os.Chmod(socketPath, 0660); err != nil { slog.Warn("pkg-helper: chmod socket failed", "error", err) } // Graceful shutdown on SIGTERM/SIGINT. sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) go func() { <-sigCh slog.Info("pkg-helper: shutting down") listener.Close() os.Remove(socketPath) os.Exit(0) }() const maxConns = 3 sem := make(chan struct{}, maxConns) slog.Info("pkg-helper: ready") for { conn, err := listener.Accept() if err != nil { break } select { case sem <- struct{}{}: go func(c net.Conn) { defer func() { <-sem }() c.SetDeadline(time.Now().Add(30 * time.Second)) //nolint:errcheck handleConn(c) }(conn) default: slog.Warn("pkg-helper: connection limit reached, rejecting") conn.Close() } } } func handleConn(conn net.Conn) { defer conn.Close() scanner := bufio.NewScanner(conn) encoder := json.NewEncoder(conn) for scanner.Scan() { var req request if err := json.Unmarshal(scanner.Bytes(), &req); err != nil { encoder.Encode(response{Error: "invalid json"}) //nolint:errcheck continue } resp := handleRequest(req) encoder.Encode(resp) //nolint:errcheck } } func handleRequest(req request) response { pkg := req.Package if pkg == "" { return response{Error: "package required"} } if !validPkgName.MatchString(pkg) { return response{Error: "invalid package name"} } switch req.Action { case "install": return doInstall(pkg) case "uninstall": return doUninstall(pkg) default: return response{Error: fmt.Sprintf("unknown action: %s", req.Action)} } } func doInstall(pkg string) response { slog.Info("pkg-helper: installing", "package", pkg) cmd := exec.Command("apk", "add", "--no-cache", pkg) out, err := cmd.CombinedOutput() if err != nil { msg := fmt.Sprintf("%s: %v", strings.TrimSpace(string(out)), err) slog.Error("pkg-helper: install failed", "package", pkg, "error", msg) return response{Error: msg} } persistAdd(pkg) slog.Info("pkg-helper: installed", "package", pkg) return response{OK: true} } func doUninstall(pkg string) response { slog.Info("pkg-helper: uninstalling", "package", pkg) cmd := exec.Command("apk", "del", pkg) out, err := cmd.CombinedOutput() if err != nil { msg := fmt.Sprintf("%s: %v", strings.TrimSpace(string(out)), err) slog.Error("pkg-helper: uninstall failed", "package", pkg, "error", msg) return response{Error: msg} } persistRemove(pkg) slog.Info("pkg-helper: uninstalled", "package", pkg) return response{OK: true} } // persistAdd appends a package name to the apk persist file (dedup check). func persistAdd(pkg string) { listFile := apkListFile() // Check if already persisted (avoid duplicates). if data, err := os.ReadFile(listFile); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.TrimSpace(line) == pkg { return // already persisted } } } f, err := os.OpenFile(listFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) if err != nil { slog.Warn("pkg-helper: persist add failed", "error", err) return } defer f.Close() fmt.Fprintln(f, pkg) } // persistRemove removes a package name from the apk persist file. // Uses write-to-temp-then-rename for atomic update (avoids truncation on disk-full). func persistRemove(pkg string) { listFile := apkListFile() data, err := os.ReadFile(listFile) if err != nil { return } var kept []string for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line != "" && line != pkg { kept = append(kept, line) } } tmpFile := listFile + ".tmp" if err := os.WriteFile(tmpFile, []byte(strings.Join(kept, "\n")+"\n"), 0640); err != nil { slog.Warn("pkg-helper: persist remove write failed", "error", err) return } if err := os.Rename(tmpFile, listFile); err != nil { slog.Warn("pkg-helper: persist remove rename failed", "error", err) os.Remove(tmpFile) //nolint:errcheck } } func apkListFile() string { runtimeDir := os.Getenv("RUNTIME_DIR") if runtimeDir == "" { runtimeDir = "/app/data/.runtime" } return runtimeDir + "/apk-packages" }