package server // The local unix socket (socket in steward.yaml, /run/steward/steward.sock by // default). It serves the same API as the network listeners, but a request // on it without an Authorization header is trusted by who sent it: the // kernel reports the connecting process's user (SO_PEERCRED), and root and // stewardd's own user get administrator rights without a token. Both can // already do everything (read the database, run 'stewardd user', stop the // service), so this adds convenience, not power: 'sudo steward status' // works on the server with nothing configured, and a lost token or // password never locks out root. // // Anyone else who can reach the socket must send an API token, which then // gives that token's permissions, as on the network. The socket file is // mode 0600 in a 0750 directory, so normally only root and the steward user // can connect at all; the user check doesn't rely on that. import ( "context" "fmt" "io/fs" "net" "net/http" "os" "os/user" "path/filepath" "strconv" "syscall" "steward/internal/auth" "steward/internal/model" ) // peer is the process at the other end of a unix socket connection. type peer struct { UID int PID int } type peerKey struct{} // peerOf returns the socket peer of a request, or nil for network // requests. func peerOf(ctx context.Context) *peer { p, _ := ctx.Value(peerKey{}).(*peer) return p } // socketConnContext records the peer's credentials on each connection to // the unix socket. func socketConnContext(ctx context.Context, c net.Conn) context.Context { uc, ok := c.(*net.UnixConn) if !ok { return ctx } p := &peer{UID: -1} if raw, err := uc.SyscallConn(); err == nil { raw.Control(func(fd uintptr) { if cred, err := syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED); err == nil { p.UID, p.PID = int(cred.Uid), int(cred.Pid) } }) } // Even when the credentials can't be read, the request is marked as a // socket request (with no trusted user). return context.WithValue(ctx, peerKey{}, p) } // trustedUID reports whether a socket peer gets administrator rights // without a token: root and the user stewardd runs as. func trustedUID(uid int) bool { return uid == 0 || uid == os.Getuid() } // socketPrincipal is the administrator for a trusted socket peer. It isn't // a Steward user (ID 0), so it has no tokens or password; the audit log and // history show it by its system user name, e.g. "root", with method // "socket". func socketPrincipal(p *peer, agent string) *auth.Principal { name := "uid " + strconv.Itoa(p.UID) if u, err := user.LookupId(strconv.Itoa(p.UID)); err == nil { name = u.Username } return &auth.Principal{User: &model.User{ID: 0, Username: name, Admin: true}, Method: "socket", IP: "local", Agent: agent} } // listenSocket creates the unix socket, replacing a stale one left by a // previous run. func listenSocket(path string) (net.Listener, error) { if len(path) > 107 { return nil, fmt.Errorf("the path is %d bytes long; unix sockets allow at most 107", len(path)) } // Under systemd the directory is RuntimeDirectory=steward; elsewhere // make it. if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return nil, err } if fi, err := os.Lstat(path); err == nil { if fi.Mode()&fs.ModeSocket == 0 { return nil, fmt.Errorf("%s exists and isn't a socket", path) } // A socket nobody answers on is left over from a previous run. if c, err := net.Dial("unix", path); err == nil { c.Close() return nil, fmt.Errorf("%s is in use by another stewardd", path) } os.Remove(path) } ln, err := net.Listen("unix", path) if err != nil { return nil, err } if err := os.Chmod(path, 0o600); err != nil { ln.Close() return nil, err } return ln, nil } // socketDenied explains a refused tokenless request on the socket. func socketDenied(p *peer) error { who := "an unknown user" if p.UID >= 0 { who = "uid " + strconv.Itoa(p.UID) if u, err := user.LookupId(strconv.Itoa(p.UID)); err == nil { who = u.Username } } return &httpError{http.StatusUnauthorized, "unauthenticated", "the local socket gives administrator rights without a token only to root and stewardd's own user, not to " + who + "; send an API token (--token), or run the command as root"} }