fix(security): prevent password exposure in process listings and config files
- commit
- 2436595db33b2be33b8ceec7d31c02e6b3d0c2d5
- author
- bnema · · 2025-10-26 11:20 UTC
- parent
- 9ef7342
- signature
- unsigned
4 files · +57 -6
M install/partitioning/format.sh +4 -0
| @@ -76,6 +76,10 @@ if [ "$ARCHUP_ENCRYPTION" = "enabled" ]; then | |||
| 76 | 76 | ||
| 77 | 77 | echo "LUKS container opened" >> "$ARCHUP_INSTALL_LOG_FILE" | |
| 78 | 78 | ||
| 79 | + | # Clear password variables from memory | |
| 80 | + | unset LUKS_PASSWORD | |
| 81 | + | unset LUKS_PASSWORD_CONFIRM | |
| 82 | + | ||
| 79 | 83 | # Export the mapped device path | |
| 80 | 84 | export ARCHUP_CRYPT_ROOT="/dev/mapper/cryptroot" | |
| 81 | 85 | ||
| @@ -76,6 +76,10 @@ if [ "$ARCHUP_ENCRYPTION" = "enabled" ]; then | |||||
| 76 | 76 | ||||
| 77 | echo "LUKS container opened" >> "$ARCHUP_INSTALL_LOG_FILE" | 77 | echo "LUKS container opened" >> "$ARCHUP_INSTALL_LOG_FILE" | ||
| 78 | 78 | ||||
| 79 | + | # Clear password variables from memory | |||
| 80 | + | unset LUKS_PASSWORD | |||
| 81 | + | unset LUKS_PASSWORD_CONFIRM | |||
| 82 | + | ||||
| 79 | # Export the mapped device path | 83 | # Export the mapped device path | ||
| 80 | export ARCHUP_CRYPT_ROOT="/dev/mapper/cryptroot" | 84 | export ARCHUP_CRYPT_ROOT="/dev/mapper/cryptroot" | ||
| 81 | 85 | ||||
M internal/config/config.go +5 -0
| @@ -300,6 +300,11 @@ func (c *Config) Save() error { | |||
| 300 | 300 | } | |
| 301 | 301 | defer file.Close() | |
| 302 | 302 | ||
| 303 | + | // Set restrictive permissions (600) since config contains passwords | |
| 304 | + | if err := os.Chmod(c.ConfigPath, 0600); err != nil { | |
| 305 | + | return fmt.Errorf("failed to set config permissions: %w", err) | |
| 306 | + | } | |
| 307 | + | ||
| 303 | 308 | writer := bufio.NewWriter(file) | |
| 304 | 309 | defer writer.Flush() | |
| 305 | 310 | ||
| @@ -300,6 +300,11 @@ func (c *Config) Save() error { | |||||
| 300 | } | 300 | } | ||
| 301 | defer file.Close() | 301 | defer file.Close() | ||
| 302 | 302 | ||||
| 303 | + | // Set restrictive permissions (600) since config contains passwords | |||
| 304 | + | if err := os.Chmod(c.ConfigPath, 0600); err != nil { | |||
| 305 | + | return fmt.Errorf("failed to set config permissions: %w", err) | |||
| 306 | + | } | |||
| 307 | + | ||||
| 303 | writer := bufio.NewWriter(file) | 308 | writer := bufio.NewWriter(file) | ||
| 304 | defer writer.Flush() | 309 | defer writer.Flush() | ||
| 305 | 310 | ||||
M internal/phases/systemconfig.go +6 -6
| @@ -182,9 +182,9 @@ func (p *ConfigPhase) configureNetwork(progressChan chan<- ProgressUpdate) error | |||
| 182 | 182 | func (p *ConfigPhase) createUser(progressChan chan<- ProgressUpdate) error { | |
| 183 | 183 | p.SendOutput(progressChan, "Creating user account...") | |
| 184 | 184 | ||
| 185 | - | // Set root password | |
| 186 | - | rootPassCmd := fmt.Sprintf("echo 'root:%s' | chpasswd", p.config.UserPassword) | |
| 187 | - | switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, rootPassCmd); { | |
| 185 | + | // Set root password using stdin (secure - not visible in process list) | |
| 186 | + | rootPassInput := fmt.Sprintf("root:%s", p.config.UserPassword) | |
| 187 | + | switch err := system.ChrootExecWithStdin(p.logger.LogPath(), config.PathMnt, "chpasswd", rootPassInput); { | |
| 188 | 188 | case err != nil: | |
| 189 | 189 | return fmt.Errorf("failed to set root password: %w", err) | |
| 190 | 190 | } | |
| @@ -196,9 +196,9 @@ func (p *ConfigPhase) createUser(progressChan chan<- ProgressUpdate) error { | |||
| 196 | 196 | return fmt.Errorf("failed to create user: %w", err) | |
| 197 | 197 | } | |
| 198 | 198 | ||
| 199 | - | // Set user password | |
| 200 | - | userPassCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", p.config.Username, p.config.UserPassword) | |
| 201 | - | switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, userPassCmd); { | |
| 199 | + | // Set user password using stdin (secure - not visible in process list) | |
| 200 | + | userPassInput := fmt.Sprintf("%s:%s", p.config.Username, p.config.UserPassword) | |
| 201 | + | switch err := system.ChrootExecWithStdin(p.logger.LogPath(), config.PathMnt, "chpasswd", userPassInput); { | |
| 202 | 202 | case err != nil: | |
| 203 | 203 | return fmt.Errorf("failed to set user password: %w", err) | |
| 204 | 204 | } | |
| @@ -182,9 +182,9 @@ func (p *ConfigPhase) configureNetwork(progressChan chan<- ProgressUpdate) error | |||||
| 182 | func (p *ConfigPhase) createUser(progressChan chan<- ProgressUpdate) error { | 182 | func (p *ConfigPhase) createUser(progressChan chan<- ProgressUpdate) error { | ||
| 183 | p.SendOutput(progressChan, "Creating user account...") | 183 | p.SendOutput(progressChan, "Creating user account...") | ||
| 184 | 184 | ||||
| 185 | - | // Set root password | 185 | + | // Set root password using stdin (secure - not visible in process list) |
| 186 | - | rootPassCmd := fmt.Sprintf("echo 'root:%s' | chpasswd", p.config.UserPassword) | 186 | + | rootPassInput := fmt.Sprintf("root:%s", p.config.UserPassword) |
| 187 | - | switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, rootPassCmd); { | 187 | + | switch err := system.ChrootExecWithStdin(p.logger.LogPath(), config.PathMnt, "chpasswd", rootPassInput); { |
| 188 | case err != nil: | 188 | case err != nil: | ||
| 189 | return fmt.Errorf("failed to set root password: %w", err) | 189 | return fmt.Errorf("failed to set root password: %w", err) | ||
| 190 | } | 190 | } | ||
| @@ -196,9 +196,9 @@ func (p *ConfigPhase) createUser(progressChan chan<- ProgressUpdate) error { | |||||
| 196 | return fmt.Errorf("failed to create user: %w", err) | 196 | return fmt.Errorf("failed to create user: %w", err) | ||
| 197 | } | 197 | } | ||
| 198 | 198 | ||||
| 199 | - | // Set user password | 199 | + | // Set user password using stdin (secure - not visible in process list) |
| 200 | - | userPassCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", p.config.Username, p.config.UserPassword) | 200 | + | userPassInput := fmt.Sprintf("%s:%s", p.config.Username, p.config.UserPassword) |
| 201 | - | switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, userPassCmd); { | 201 | + | switch err := system.ChrootExecWithStdin(p.logger.LogPath(), config.PathMnt, "chpasswd", userPassInput); { |
| 202 | case err != nil: | 202 | case err != nil: | ||
| 203 | return fmt.Errorf("failed to set user password: %w", err) | 203 | return fmt.Errorf("failed to set user password: %w", err) | ||
| 204 | } | 204 | } | ||
M internal/system/chroot.go +42 -0
| @@ -2,6 +2,8 @@ package system | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "os" | |
| 6 | + | "os/exec" | |
| 5 | 7 | "strings" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| @@ -20,6 +22,46 @@ func ChrootExec(logPath, mountPoint, command string, args ...string) error { | |||
| 20 | 22 | return nil | |
| 21 | 23 | } | |
| 22 | 24 | ||
| 25 | + | // ChrootExecWithStdin executes a command inside chroot with stdin input | |
| 26 | + | // This is secure for passing sensitive data like passwords without exposing them in process listings | |
| 27 | + | func ChrootExecWithStdin(logPath, mountPoint, command, stdin string) error { | |
| 28 | + | cmd := exec.Command("arch-chroot", mountPoint, "bash", "-c", command) | |
| 29 | + | ||
| 30 | + | // Set up stdin pipe | |
| 31 | + | stdinPipe, err := cmd.StdinPipe() | |
| 32 | + | if err != nil { | |
| 33 | + | return fmt.Errorf("failed to create stdin pipe: %w", err) | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // Set up logging | |
| 37 | + | if logPath != "" { | |
| 38 | + | logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | |
| 39 | + | if err == nil { | |
| 40 | + | defer logFile.Close() | |
| 41 | + | cmd.Stdout = logFile | |
| 42 | + | cmd.Stderr = logFile | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Start the command | |
| 47 | + | if err := cmd.Start(); err != nil { | |
| 48 | + | return fmt.Errorf("failed to start command: %w", err) | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // Write to stdin and close pipe | |
| 52 | + | if _, err := stdinPipe.Write([]byte(stdin)); err != nil { | |
| 53 | + | return fmt.Errorf("failed to write to stdin: %w", err) | |
| 54 | + | } | |
| 55 | + | stdinPipe.Close() | |
| 56 | + | ||
| 57 | + | // Wait for command to complete | |
| 58 | + | if err := cmd.Wait(); err != nil { | |
| 59 | + | return fmt.Errorf("failed to execute in chroot: %w", err) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | return nil | |
| 63 | + | } | |
| 64 | + | ||
| 23 | 65 | // ChrootPacman runs pacman inside the chroot | |
| 24 | 66 | func ChrootPacman(logPath, mountPoint, operation string, packages ...string) error { | |
| 25 | 67 | args := append([]string{operation, "--noconfirm"}, packages...) | |
| @@ -2,6 +2,8 @@ package system | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "os" | |||
| 6 | + | "os/exec" | |||
| 5 | "strings" | 7 | "strings" | ||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| @@ -20,6 +22,46 @@ func ChrootExec(logPath, mountPoint, command string, args ...string) error { | |||||
| 20 | return nil | 22 | return nil | ||
| 21 | } | 23 | } | ||
| 22 | 24 | ||||
| 25 | + | // ChrootExecWithStdin executes a command inside chroot with stdin input | |||
| 26 | + | // This is secure for passing sensitive data like passwords without exposing them in process listings | |||
| 27 | + | func ChrootExecWithStdin(logPath, mountPoint, command, stdin string) error { | |||
| 28 | + | cmd := exec.Command("arch-chroot", mountPoint, "bash", "-c", command) | |||
| 29 | + | ||||
| 30 | + | // Set up stdin pipe | |||
| 31 | + | stdinPipe, err := cmd.StdinPipe() | |||
| 32 | + | if err != nil { | |||
| 33 | + | return fmt.Errorf("failed to create stdin pipe: %w", err) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // Set up logging | |||
| 37 | + | if logPath != "" { | |||
| 38 | + | logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | |||
| 39 | + | if err == nil { | |||
| 40 | + | defer logFile.Close() | |||
| 41 | + | cmd.Stdout = logFile | |||
| 42 | + | cmd.Stderr = logFile | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Start the command | |||
| 47 | + | if err := cmd.Start(); err != nil { | |||
| 48 | + | return fmt.Errorf("failed to start command: %w", err) | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // Write to stdin and close pipe | |||
| 52 | + | if _, err := stdinPipe.Write([]byte(stdin)); err != nil { | |||
| 53 | + | return fmt.Errorf("failed to write to stdin: %w", err) | |||
| 54 | + | } | |||
| 55 | + | stdinPipe.Close() | |||
| 56 | + | ||||
| 57 | + | // Wait for command to complete | |||
| 58 | + | if err := cmd.Wait(); err != nil { | |||
| 59 | + | return fmt.Errorf("failed to execute in chroot: %w", err) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | return nil | |||
| 63 | + | } | |||
| 64 | + | ||||
| 23 | // ChrootPacman runs pacman inside the chroot | 65 | // ChrootPacman runs pacman inside the chroot | ||
| 24 | func ChrootPacman(logPath, mountPoint, operation string, packages ...string) error { | 66 | func ChrootPacman(logPath, mountPoint, operation string, packages ...string) error { | ||
| 25 | args := append([]string{operation, "--noconfirm"}, packages...) | 67 | args := append([]string{operation, "--noconfirm"}, packages...) | ||