jarld also listens on /run/jarl/jarl.sock (socket in jarl.yaml). Requests on it from root or jarld's own user need no token and act as an administrator; the kernel's SO_PEERCRED says who is connecting, and anyone else must send a token. The jarl CLI uses the socket when no server is configured, or with -s local, so 'sudo jarl status' works on the server with nothing set up and a lost token or password never locks root out. Changes are audited under the system user's name via "socket". The systemd unit creates /run/jarl (0750). Socket paths over 107 bytes are refused with a clear message. The docs explain the socket and what happens when a token is revoked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package server
|
|
|
|
// The local unix socket (socket in jarl.yaml, /run/jarl/jarl.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
|
|
// jarld's own user get administrator rights without a token. Both can
|
|
// already do everything (read the database, run 'jarld user token', edit
|
|
// named.conf), so this adds convenience, not power: 'sudo jarl 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 jarl user
|
|
// can connect at all; the user check doesn't rely on that.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/user"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"jarl/internal/auth"
|
|
"jarl/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 jarld 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 Jarl 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))
|
|
}
|
|
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 jarld", 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 jarld's own user, not to " + who + "; send an API token (--token), or run the command as root"}
|
|
}
|