diff --git a/authdaemon/connection.go b/authdaemon/connection.go new file mode 100644 index 0000000..9d36e27 --- /dev/null +++ b/authdaemon/connection.go @@ -0,0 +1,27 @@ +package authdaemon + +import ( + "github.com/fosrl/newt/logger" +) + +// ProcessConnection runs the same logic as POST /connection: CA cert, user create/reconcile, principals. +// Use this when DisableHTTPS is true (e.g. embedded in Newt) instead of calling the API. +func (s *Server) ProcessConnection(req ConnectionRequest) { + logger.Info("connection: niceId=%q username=%q metadata.sudo=%v metadata.homedir=%v", + req.NiceId, req.Username, req.Metadata.Sudo, req.Metadata.Homedir) + + cfg := &s.cfg + if cfg.CACertPath != "" { + if err := writeCACertIfNotExists(cfg.CACertPath, req.CaCert, cfg.Force); err != nil { + logger.Warn("auth-daemon: write CA cert: %v", err) + } + } + if err := ensureUser(req.Username, req.Metadata); err != nil { + logger.Warn("auth-daemon: ensure user: %v", err) + } + if cfg.PrincipalsFilePath != "" { + if err := writePrincipals(cfg.PrincipalsFilePath, req.Username, req.NiceId); err != nil { + logger.Warn("auth-daemon: write principals: %v", err) + } + } +} diff --git a/authdaemon/host_linux.go b/authdaemon/host_linux.go index 4416dd1..82834f3 100644 --- a/authdaemon/host_linux.go +++ b/authdaemon/host_linux.go @@ -16,20 +16,33 @@ import ( "github.com/fosrl/newt/logger" ) -// writeCACertIfNotExists writes contents to path only if the file does not exist. -func writeCACertIfNotExists(path, contents string) error { - if _, err := os.Stat(path); err == nil { - logger.Debug("auth-daemon: CA cert already exists at %s, skipping write", path) - return nil +// writeCACertIfNotExists writes contents to path. If the file already exists: when force is false, skip; when force is true, overwrite only if content differs. +func writeCACertIfNotExists(path, contents string, force bool) error { + contents = strings.TrimSpace(contents) + if contents != "" && !strings.HasSuffix(contents, "\n") { + contents += "\n" + } + existing, err := os.ReadFile(path) + if err == nil { + existingStr := strings.TrimSpace(string(existing)) + if existingStr != "" && !strings.HasSuffix(existingStr, "\n") { + existingStr += "\n" + } + if existingStr == contents { + logger.Debug("auth-daemon: CA cert unchanged at %s, skipping write", path) + return nil + } + if !force { + logger.Debug("auth-daemon: CA cert already exists at %s, skipping write (Force disabled)", path) + return nil + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("read %s: %w", path, err) } dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("mkdir %s: %w", dir, err) } - contents = strings.TrimSpace(contents) - if contents != "" && !strings.HasSuffix(contents, "\n") { - contents += "\n" - } if err := os.WriteFile(path, []byte(contents), 0644); err != nil { return fmt.Errorf("write CA cert: %w", err) } @@ -37,64 +50,6 @@ func writeCACertIfNotExists(path, contents string) error { return nil } -// ensureSSHDTrustedUserCAKeys ensures sshd_config contains TrustedUserCAKeys caCertPath. -func ensureSSHDTrustedUserCAKeys(sshdConfigPath, caCertPath string) error { - if sshdConfigPath == "" { - sshdConfigPath = "/etc/ssh/sshd_config" - } - data, err := os.ReadFile(sshdConfigPath) - if err != nil { - return fmt.Errorf("read sshd_config: %w", err) - } - directive := "TrustedUserCAKeys " + caCertPath - lines := strings.Split(string(data), "\n") - found := false - for i, line := range lines { - trimmed := strings.TrimSpace(line) - // strip inline comment - if idx := strings.Index(trimmed, "#"); idx >= 0 { - trimmed = strings.TrimSpace(trimmed[:idx]) - } - if trimmed == "" { - continue - } - if strings.HasPrefix(trimmed, "TrustedUserCAKeys") { - if strings.TrimSpace(trimmed) == directive { - logger.Debug("auth-daemon: sshd_config already has TrustedUserCAKeys %s", caCertPath) - return nil - } - lines[i] = directive - found = true - break - } - } - if !found { - lines = append(lines, directive) - } - out := strings.Join(lines, "\n") - if !strings.HasSuffix(out, "\n") { - out += "\n" - } - if err := os.WriteFile(sshdConfigPath, []byte(out), 0644); err != nil { - return fmt.Errorf("write sshd_config: %w", err) - } - logger.Info("auth-daemon: updated %s with TrustedUserCAKeys %s", sshdConfigPath, caCertPath) - return nil -} - -// reloadSSHD runs the given shell command to reload sshd (e.g. "systemctl reload sshd"). -func reloadSSHD(reloadCmd string) error { - if reloadCmd == "" { - return nil - } - cmd := exec.Command("sh", "-c", reloadCmd) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("reload sshd %q: %w (output: %s)", reloadCmd, err, string(out)) - } - logger.Info("auth-daemon: reloaded sshd") - return nil -} - // writePrincipals updates the principals file at path: JSON object keyed by username, value is array of principals. Adds username and niceId to that user's list (deduped). func writePrincipals(path, username, niceId string) error { if path == "" { diff --git a/authdaemon/host_stub.go b/authdaemon/host_stub.go index dfd09a5..2fb4c1a 100644 --- a/authdaemon/host_stub.go +++ b/authdaemon/host_stub.go @@ -7,17 +7,7 @@ import "fmt" var errLinuxOnly = fmt.Errorf("auth-daemon PAM agent is only supported on Linux") // writeCACertIfNotExists returns an error on non-Linux. -func writeCACertIfNotExists(path, contents string) error { - return errLinuxOnly -} - -// ensureSSHDTrustedUserCAKeys returns an error on non-Linux. -func ensureSSHDTrustedUserCAKeys(sshdConfigPath, caCertPath string) error { - return errLinuxOnly -} - -// reloadSSHD returns an error on non-Linux. -func reloadSSHD(reloadCmd string) error { +func writeCACertIfNotExists(path, contents string, force bool) error { return errLinuxOnly } diff --git a/authdaemon/routes.go b/authdaemon/routes.go index d7ce880..2cccc54 100644 --- a/authdaemon/routes.go +++ b/authdaemon/routes.go @@ -3,8 +3,6 @@ package authdaemon import ( "encoding/json" "net/http" - - "github.com/fosrl/newt/logger" ) // registerRoutes registers all API routes. Add new endpoints here. @@ -42,40 +40,6 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(healthResponse{Status: "ok"}) } -// ProcessConnection runs the same logic as POST /connection: CA cert, sshd config, user create/reconcile, principals. -// Use this when DisableHTTPS is true (e.g. embedded in Newt) instead of calling the API. -func (s *Server) ProcessConnection(req ConnectionRequest) { - logger.Info("connection: niceId=%q username=%q metadata.sudo=%v metadata.homedir=%v", - req.NiceId, req.Username, req.Metadata.Sudo, req.Metadata.Homedir) - - cfg := &s.cfg - if cfg.CACertPath != "" { - if err := writeCACertIfNotExists(cfg.CACertPath, req.CaCert); err != nil { - logger.Warn("auth-daemon: write CA cert: %v", err) - } - sshdConfig := cfg.SSHDConfigPath - if sshdConfig == "" { - sshdConfig = "/etc/ssh/sshd_config" - } - if err := ensureSSHDTrustedUserCAKeys(sshdConfig, cfg.CACertPath); err != nil { - logger.Warn("auth-daemon: sshd_config: %v", err) - } - if cfg.ReloadSSHCommand != "" { - if err := reloadSSHD(cfg.ReloadSSHCommand); err != nil { - logger.Warn("auth-daemon: reload sshd: %v", err) - } - } - } - if err := ensureUser(req.Username, req.Metadata); err != nil { - logger.Warn("auth-daemon: ensure user: %v", err) - } - if cfg.PrincipalsFilePath != "" { - if err := writePrincipals(cfg.PrincipalsFilePath, req.Username, req.NiceId); err != nil { - logger.Warn("auth-daemon: write principals: %v", err) - } - } -} - // handleConnection accepts POST with connection payload and delegates to ProcessConnection. func (s *Server) handleConnection(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/authdaemon/server.go b/authdaemon/server.go index 83cb480..78aa908 100644 --- a/authdaemon/server.go +++ b/authdaemon/server.go @@ -24,12 +24,11 @@ import ( type Config struct { // DisableHTTPS: when true, Run() does not start the HTTPS server (for embedded use inside Newt). Call ProcessConnection directly for connection events. DisableHTTPS bool - Port int // Listen port for the HTTPS server. Required when DisableHTTPS is false. - PresharedKey string // Required when DisableHTTPS is false; used for HTTP auth (Authorization: Bearer or X-Preshared-Key: ). - CACertPath string // Where to write the CA cert (e.g. /etc/ssh/ca.pem). - SSHDConfigPath string // Path to sshd_config (e.g. /etc/ssh/sshd_config). Defaults to /etc/ssh/sshd_config when CACertPath is set. - ReloadSSHCommand string // Command to reload sshd after config change (e.g. "systemctl reload sshd"). Empty = no reload. - PrincipalsFilePath string // Path to the principals data file (JSON: username -> array of principals). Empty = do not store principals. + Port int // Required when DisableHTTPS is false. Listen port for the HTTPS server. No default. + PresharedKey string // Required when DisableHTTPS is false. HTTP auth (Authorization: Bearer or X-Preshared-Key: ). No default. + CACertPath string // Required. Where to write the CA cert (e.g. /etc/ssh/ca.pem). No default. + Force bool // If true, overwrite existing CA cert (and other items) when content differs. Default false. + PrincipalsFilePath string // Required. Path to the principals data file (JSON: username -> array of principals). No default. } type Server struct { @@ -98,18 +97,24 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler { }) } -// NewServer builds a new auth-daemon server from cfg. When DisableHTTPS is false, PresharedKey and Port are required. +// NewServer builds a new auth-daemon server from cfg. Port, PresharedKey, CACertPath, and PrincipalsFilePath are required (no defaults). func NewServer(cfg Config) (*Server, error) { if runtime.GOOS != "linux" { return nil, fmt.Errorf("auth-daemon is only supported on Linux, not %s", runtime.GOOS) } if !cfg.DisableHTTPS { - if cfg.PresharedKey == "" { - return nil, fmt.Errorf("preshared key is required when HTTPS is enabled") - } if cfg.Port <= 0 { - return nil, fmt.Errorf("port must be positive when HTTPS is enabled") + return nil, fmt.Errorf("port is required and must be positive") } + if cfg.PresharedKey == "" { + return nil, fmt.Errorf("preshared key is required") + } + } + if cfg.CACertPath == "" { + return nil, fmt.Errorf("CACertPath is required") + } + if cfg.PrincipalsFilePath == "" { + return nil, fmt.Errorf("PrincipalsFilePath is required") } s := &Server{cfg: cfg} if !cfg.DisableHTTPS {