diff --git a/authdaemon.go b/authdaemon.go index 5de92ad..dc6d313 100644 --- a/authdaemon.go +++ b/authdaemon.go @@ -46,11 +46,12 @@ func startAuthDaemon(ctx context.Context) error { // Create auth daemon server cfg := authdaemon.Config{ - DisableHTTPS: true, // We run without HTTP server in newt - PresharedKey: "this-key-is-not-used", // Not used in embedded mode, but set to non-empty to satisfy validation - PrincipalsFilePath: principalsFile, - CACertPath: caCertPath, - Force: true, + DisableHTTPS: true, // We run without HTTP server in newt + PresharedKey: "this-key-is-not-used", // Not used in embedded mode, but set to non-empty to satisfy validation + PrincipalsFilePath: principalsFile, + CACertPath: caCertPath, + Force: true, + GenerateRandomPassword: authDaemonGenerateRandomPassword, } srv, err := authdaemon.NewServer(cfg) @@ -72,8 +73,6 @@ func startAuthDaemon(ctx context.Context) error { return nil } - - // runPrincipalsCmd executes the principals subcommand logic func runPrincipalsCmd(args []string) { opts := struct { @@ -148,4 +147,4 @@ Example: newt principals --username alice `, defaultPrincipalsPath) -} \ No newline at end of file +} diff --git a/authdaemon/connection.go b/authdaemon/connection.go index 107d25d..3fb44c5 100644 --- a/authdaemon/connection.go +++ b/authdaemon/connection.go @@ -16,7 +16,7 @@ func (s *Server) ProcessConnection(req ConnectionRequest) { logger.Warn("auth-daemon: write CA cert: %v", err) } } - if err := ensureUser(req.Username, req.Metadata); err != nil { + if err := ensureUser(req.Username, req.Metadata, s.cfg.GenerateRandomPassword); err != nil { logger.Warn("auth-daemon: ensure user: %v", err) } if cfg.PrincipalsFilePath != "" { diff --git a/authdaemon/host_linux.go b/authdaemon/host_linux.go index fef15b8..12af717 100644 --- a/authdaemon/host_linux.go +++ b/authdaemon/host_linux.go @@ -4,6 +4,8 @@ package authdaemon import ( "bufio" + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" "os" @@ -122,6 +124,22 @@ func sudoGroup() string { return "sudo" } +// setRandomPassword generates a random password and sets it for username via chpasswd. +// Used when GenerateRandomPassword is true so SSH with PermitEmptyPasswords no can accept the user. +func setRandomPassword(username string) error { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return fmt.Errorf("generate password: %w", err) + } + password := hex.EncodeToString(b) + cmd := exec.Command("chpasswd") + cmd.Stdin = strings.NewReader(username + ":" + password) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("chpasswd: %w (output: %s)", err, string(out)) + } + return nil +} + const skelDir = "/etc/skel" // copySkelInto copies files from srcDir (e.g. /etc/skel) into dstDir (e.g. user's home). @@ -172,7 +190,7 @@ func copySkelInto(srcDir, dstDir string, uid, gid int) { } // ensureUser creates the system user if missing, or reconciles sudo and homedir to match meta. -func ensureUser(username string, meta ConnectionMetadata) error { +func ensureUser(username string, meta ConnectionMetadata, generateRandomPassword bool) error { if username == "" { return nil } @@ -181,7 +199,7 @@ func ensureUser(username string, meta ConnectionMetadata) error { if _, ok := err.(user.UnknownUserError); !ok { return fmt.Errorf("lookup user %s: %w", username, err) } - return createUser(username, meta) + return createUser(username, meta, generateRandomPassword) } return reconcileUser(u, meta) } @@ -223,7 +241,7 @@ func setUserGroups(username string, groups []string) { } } -func createUser(username string, meta ConnectionMetadata) error { +func createUser(username string, meta ConnectionMetadata, generateRandomPassword bool) error { args := []string{"-s", "/bin/bash"} if meta.Homedir { args = append(args, "-m") @@ -236,6 +254,13 @@ func createUser(username string, meta ConnectionMetadata) error { return fmt.Errorf("useradd %s: %w (output: %s)", username, err, string(out)) } logger.Info("auth-daemon: created user %s (homedir=%v)", username, meta.Homedir) + if generateRandomPassword { + if err := setRandomPassword(username); err != nil { + logger.Warn("auth-daemon: set random password for %s: %v", username, err) + } else { + logger.Info("auth-daemon: set random password for %s (PermitEmptyPasswords no)", username) + } + } if meta.Homedir { if u, err := user.Lookup(username); err == nil && u.HomeDir != "" { uid, gid := mustAtoi(u.Uid), mustAtoi(u.Gid) diff --git a/authdaemon/host_stub.go b/authdaemon/host_stub.go index 2fb4c1a..492d2cd 100644 --- a/authdaemon/host_stub.go +++ b/authdaemon/host_stub.go @@ -12,7 +12,7 @@ func writeCACertIfNotExists(path, contents string, force bool) error { } // ensureUser returns an error on non-Linux. -func ensureUser(username string, meta ConnectionMetadata) error { +func ensureUser(username string, meta ConnectionMetadata, generateRandomPassword bool) error { return errLinuxOnly } diff --git a/authdaemon/server.go b/authdaemon/server.go index 78aa908..224ac9e 100644 --- a/authdaemon/server.go +++ b/authdaemon/server.go @@ -27,8 +27,9 @@ type Config struct { 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. + 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. + GenerateRandomPassword bool // If true, set a random password on users when they are provisioned (for SSH PermitEmptyPasswords no). } type Server struct { diff --git a/main.go b/main.go index 4b9fdd6..dee958a 100644 --- a/main.go +++ b/main.go @@ -137,6 +137,7 @@ var ( authDaemonPrincipalsFile string authDaemonCACertPath string authDaemonEnabled bool + authDaemonGenerateRandomPassword bool // Build/version (can be overridden via -ldflags "-X main.newtVersion=...") newtVersion = "version_replaceme" @@ -216,6 +217,7 @@ func runNewtMain(ctx context.Context) { authDaemonPrincipalsFile = os.Getenv("AD_PRINCIPALS_FILE") authDaemonCACertPath = os.Getenv("AD_CA_CERT_PATH") authDaemonEnabledEnv := os.Getenv("AUTH_DAEMON_ENABLED") + authDaemonGenerateRandomPasswordEnv := os.Getenv("AD_GENERATE_RANDOM_PASSWORD") // Metrics/observability env mirrors metricsEnabledEnv := os.Getenv("NEWT_METRICS_PROMETHEUS_ENABLED") @@ -421,6 +423,13 @@ func runNewtMain(ctx context.Context) { authDaemonEnabled = v } } + if authDaemonGenerateRandomPasswordEnv == "" { + flag.BoolVar(&authDaemonGenerateRandomPassword, "ad-generate-random-password", false, "Generate a random password for authenticated users") + } else { + if v, err := strconv.ParseBool(authDaemonGenerateRandomPasswordEnv); err == nil { + authDaemonGenerateRandomPassword = v + } + } // do a --version check version := flag.Bool("version", false, "Print the version")