Add a local unix socket for root on the server

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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-08 15:10:57 +00:00
1 parent f528c2aa9c
commit cf49df0346
22 files changed
+570 -26

No files matched your search

+44 -1
View File
@@ -40,6 +40,8 @@ type Config struct {
Stats *stats.Collector
// HTTPReq configures the endpoint for lego's httpreq DNS provider.
HTTPReq HTTPReq
// Socket is the local unix socket's path ("" for none); see socket.go.
Socket string
}
// Server is the HTTP server.
@@ -104,7 +106,7 @@ func (s *Server) Serve(ctx context.Context, ready func()) error {
s.certs = cr
tlsCfg = &tls.Config{MinVersion: tls.VersionTLS12, GetCertificate: cr.get}
}
errc := make(chan error, len(s.cfg.Listen))
errc := make(chan error, len(s.cfg.Listen)+1)
var listeners []net.Listener
for _, addr := range s.cfg.Listen {
ln, err := net.Listen("tcp", addr)
@@ -155,6 +157,7 @@ func (s *Server) Serve(ctx context.Context, ready func()) error {
}
}
}
s.serveSocket(errc)
s.logHTTPReq(tlsCfg != nil)
if ready != nil {
ready()
@@ -170,6 +173,37 @@ func (s *Server) Serve(ctx context.Context, ready func()) error {
return nil
}
// serveSocket serves the API on the local unix socket. A socket that
// can't be created (e.g. /run/jarl missing outside systemd) is logged, not
// fatal: the network listeners still work.
func (s *Server) serveSocket(errc chan error) {
if s.cfg.Socket == "" {
return
}
ln, err := listenSocket(s.cfg.Socket)
if err != nil {
s.log.Warn("not listening on the local socket", "path", s.cfg.Socket, "err", err)
return
}
srv := &http.Server{
Handler: s.Handler(),
ConnContext: socketConnContext,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 2 * time.Minute,
ErrorLog: slog.NewLogLogger(s.log.Handler(), slog.LevelWarn),
}
s.mu.Lock()
s.srvs = append(s.srvs, srv)
s.mu.Unlock()
s.log.Info("listening", "socket", s.cfg.Socket)
go func() {
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
errc <- err
}
}()
}
func (s *Server) shutdown() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
@@ -178,6 +212,9 @@ func (s *Server) shutdown() {
for _, srv := range s.srvs {
srv.Shutdown(ctx)
}
if s.cfg.Socket != "" {
os.Remove(s.cfg.Socket)
}
}
// ReloadTLS rereads the TLS certificate (on SIGHUP).
@@ -344,6 +381,9 @@ func (s *Server) logRequests(next http.Handler) http.Handler {
// clientIP returns the client's address, honoring X-Forwarded-For from
// trusted proxies.
func (s *Server) clientIP(r *http.Request) string {
if peerOf(r.Context()) != nil {
return "local"
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
@@ -413,6 +453,9 @@ func (s *Server) transport(r *http.Request) (https bool, why string) {
if r.TLS != nil {
return true, "HTTPS to jarld (tls in jarl.yaml)"
}
if peerOf(r.Context()) != nil {
return true, "jarld's local unix socket (never crosses a network)"
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr