fix(ssh): perform root command elevation test once before password input#204
Conversation
76de293 to
37104fe
Compare
775ec76 to
8a99a36
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughThis PR refactors root command elevation from simple string fields to a new Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.11.4)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmd/apply/apply.go (1)
314-323:⚠️ Potential issue | 🟠 MajorRe-exec still ignores
root.password_method.
ExecAsRoot(cfg.Root.Command)only prefixes the bare command, sopassword_method = nonenever adds-n, and the new stdin/TTY modes are skipped entirely. That leavesapply.reexec_as_root—and the same helper used by generation delete/switch/rollback—outside the newRootElevatorbehavior.This needs `ExecAsRoot` to accept variadic extra argv entries.Possible direction
- if err = utils.ExecAsRoot(cfg.Root.Command); err != nil { + elevator, elevErr := system.ElevatorFromConfig(&cfg.Root) + if elevErr != nil { + log.Error(elevErr) + return elevErr + } + if err = utils.ExecAsRoot(elevator.Command, elevator.Flags...); err != nil { log.Errorf("failed to re-exec command as root: %v", err) return err }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/apply/apply.go` around lines 314 - 323, The re-exec path uses ExecAsRoot(cfg.Root.Command) which ignores cfg.Root.PasswordMethod and new RootElevator behavior, so update ExecAsRoot to accept variadic extra argv entries (e.g., ExecAsRoot(cmd string, extra ...string)) and propagate the configured flags/TTY/stdin options when building the final argv; then change callers (like the re-exec in apply.go where ExecAsRoot is invoked) to pass the appropriate extra arguments (such as "-n" or other elevator-specific tokens) derived from cfg.Root and RootElevator so password_method and TTY handling are honored by ExecAsRoot and reuse the same helper across apply/generate delete/switch/rollback.
🧹 Nitpick comments (4)
internal/system/runner_test.go (1)
98-107: Cover the new stdin-handling modes too.This only checks argv prefixing. The new behavior is in
internal/system/local.goforPasswordInputMethodStdinandPasswordInputMethodNone, so a regression there would still pass this test.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/system/runner_test.go` around lines 98 - 107, The current TestBuildShellWrapper_WithRootElevation only verifies argv prefixing; extend it to also exercise the stdin-handling modes defined in internal/system/local.go by adding sub-tests for PasswordInputMethodStdin and PasswordInputMethodNone using the same RootElevator and NewCommand/AsRoot flow: for PasswordInputMethodStdin assert the resulting wrapped command is configured to read from stdin (e.g., expects a pipe or includes the stdin flag/marker your wrapper uses), and for PasswordInputMethodNone assert it does not require stdin and that no stdin piping/marker is present; reference the RootElevator struct, PasswordInputMethodStdin, PasswordInputMethodNone, NewCommand, AsRoot and the TestBuildShellWrapper_WithRootElevation test to locate and add these assertions.internal/settings/settings.go (1)
91-97: Consider addingUnmarshalTextforPasswordInputMethodfor robust parsing.Unlike
DiffToolandConfirmationPromptBehaviorwhich haveUnmarshalTextmethods,PasswordInputMethodrelies on direct string assignment. Invalid values like"invalid"would be silently accepted during config parsing.🔧 Add UnmarshalText for validation
func (p *PasswordInputMethod) UnmarshalText(text []byte) error { v := PasswordInputMethod(text) switch v { case PasswordInputMethodStdin, PasswordInputMethodTTY, PasswordInputMethodNone: *p = v return nil default: return fmt.Errorf("invalid value for password_method '%s'", text) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/settings/settings.go` around lines 91 - 97, Add an UnmarshalText method to PasswordInputMethod to validate parsed values instead of accepting arbitrary strings: implement func (p *PasswordInputMethod) UnmarshalText(text []byte) error that converts text to PasswordInputMethod, checks it against the allowed constants PasswordInputMethodStdin, PasswordInputMethodTTY, and PasswordInputMethodNone, sets *p on success, and returns a descriptive error (e.g. "invalid value for password_method '%s'") for any other value so config parsing fails on invalid inputs.internal/system/elevator.go (2)
81-107: Consider zeroing the password after use for defense in depth.The
CachedPasswordProviderstores the password as a plain string in memory. While this is often acceptable for short-lived CLI tools, zeroing the password after the process completes could reduce the window for memory scraping attacks.This is a low-priority security hardening suggestion given the CLI's typical usage pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/system/elevator.go` around lines 81 - 107, CachedPasswordProvider currently retains the password in memory; add a Clear (or Dispose) method on CachedPasswordProvider that securely zeroes the stored password (convert the string to a byte slice, overwrite bytes with zeros, then set p.password = "" and p.set = false) and ensure callers that finish using the password invoke this Clear method; update any code paths that call GetPassword/PromptForPassword to call Clear after elevation completes to reduce exposure time.
71-75: Consider logging the error fromGetUsername()for debugging.The error from
utils.GetUsername()is silently discarded. While the fallback prompt is reasonable, logging at debug level would help troubleshoot issues.🔧 Suggested improvement
- if currentUser, _ := utils.GetUsername(); currentUser != "" { + if currentUser, err := utils.GetUsername(); err == nil && currentUser != "" { prompt = fmt.Sprintf("[%s] password for %s: ", e.Command, currentUser) } else { + // Debug log: failed to get username, using generic prompt prompt = fmt.Sprintf("[%s] enter password: ", e.Command) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/system/elevator.go` around lines 71 - 75, Capture and check the error returned from utils.GetUsername() instead of discarding it, and log it at debug level for troubleshooting; modify the block using utils.GetUsername() (where currentUser is assigned and prompt is set) to use "currentUser, err := utils.GetUsername()" and if err != nil call the elevator's logger (e.g., e.Logger.Debugf("utils.GetUsername error: %v", err)) before falling back to the existing prompt logic that uses e.Command and prompt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/apply/apply.go`:
- Around line 356-363: The rootElevator is only created when os.Geteuid() != 0
which can leave rootElevator nil while activationUseRoot (the flag deciding
remote/local elevation) is true; change the creation logic so that you call
system.ElevatorFromConfig(&cfg.Root) and set rootElevator whenever elevation is
requested (i.e., when activationUseRoot or the equivalent flag/condition for
remote/root elevation is true), not just when Geteuid() != 0; ensure you still
handle and return the error from system.ElevatorFromConfig(&cfg.Root) the same
way as before so activation paths that call AsRoot(...) have a non-nil elevator
when UseRootCommand is true.
In `@internal/utils/utils.go`:
- Around line 142-176: PromptForPassword currently reads from stdin so the
goroutine blocks even if ctx is cancelled; change it to open and read from a
cancellable descriptor (e.g. os.Open("/dev/tty")) and pass that fd to
term.ReadPassword so you can close the file when ctx.Done() fires; ensure you
call term.Restore(fd, oldState) and close the /dev/tty file on cancel, and still
restore and close on normal completion; update references in the function
(PromptForPassword, term.ReadPassword, term.Restore) accordingly so the
goroutine stops when the context is cancelled.
---
Outside diff comments:
In `@cmd/apply/apply.go`:
- Around line 314-323: The re-exec path uses ExecAsRoot(cfg.Root.Command) which
ignores cfg.Root.PasswordMethod and new RootElevator behavior, so update
ExecAsRoot to accept variadic extra argv entries (e.g., ExecAsRoot(cmd string,
extra ...string)) and propagate the configured flags/TTY/stdin options when
building the final argv; then change callers (like the re-exec in apply.go where
ExecAsRoot is invoked) to pass the appropriate extra arguments (such as "-n" or
other elevator-specific tokens) derived from cfg.Root and RootElevator so
password_method and TTY handling are honored by ExecAsRoot and reuse the same
helper across apply/generate delete/switch/rollback.
---
Nitpick comments:
In `@internal/settings/settings.go`:
- Around line 91-97: Add an UnmarshalText method to PasswordInputMethod to
validate parsed values instead of accepting arbitrary strings: implement func (p
*PasswordInputMethod) UnmarshalText(text []byte) error that converts text to
PasswordInputMethod, checks it against the allowed constants
PasswordInputMethodStdin, PasswordInputMethodTTY, and PasswordInputMethodNone,
sets *p on success, and returns a descriptive error (e.g. "invalid value for
password_method '%s'") for any other value so config parsing fails on invalid
inputs.
In `@internal/system/elevator.go`:
- Around line 81-107: CachedPasswordProvider currently retains the password in
memory; add a Clear (or Dispose) method on CachedPasswordProvider that securely
zeroes the stored password (convert the string to a byte slice, overwrite bytes
with zeros, then set p.password = "" and p.set = false) and ensure callers that
finish using the password invoke this Clear method; update any code paths that
call GetPassword/PromptForPassword to call Clear after elevation completes to
reduce exposure time.
- Around line 71-75: Capture and check the error returned from
utils.GetUsername() instead of discarding it, and log it at debug level for
troubleshooting; modify the block using utils.GetUsername() (where currentUser
is assigned and prompt is set) to use "currentUser, err := utils.GetUsername()"
and if err != nil call the elevator's logger (e.g.,
e.Logger.Debugf("utils.GetUsername error: %v", err)) before falling back to the
existing prompt logic that uses e.Command and prompt.
In `@internal/system/runner_test.go`:
- Around line 98-107: The current TestBuildShellWrapper_WithRootElevation only
verifies argv prefixing; extend it to also exercise the stdin-handling modes
defined in internal/system/local.go by adding sub-tests for
PasswordInputMethodStdin and PasswordInputMethodNone using the same RootElevator
and NewCommand/AsRoot flow: for PasswordInputMethodStdin assert the resulting
wrapped command is configured to read from stdin (e.g., expects a pipe or
includes the stdin flag/marker your wrapper uses), and for
PasswordInputMethodNone assert it does not require stdin and that no stdin
piping/marker is present; reference the RootElevator struct,
PasswordInputMethodStdin, PasswordInputMethodNone, NewCommand, AsRoot and the
TestBuildShellWrapper_WithRootElevation test to locate and add these assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a695fb03-0001-4979-b4d9-54eee9893419
📒 Files selected for processing (12)
cmd/apply/apply.gocmd/generation/delete/delete.gocmd/generation/rollback/rollback.gocmd/generation/switch/switch.gointernal/activation/activation.gointernal/settings/settings.gointernal/system/elevator.gointernal/system/local.gointernal/system/runner.gointernal/system/runner_test.gointernal/system/ssh.gointernal/utils/utils.go
8a99a36 to
237fcbf
Compare
There is a litany of bugs that exist with current root elevation mechanisms.
sudoinvocations) often dispose of the first character; this only applies to the second command ran with a single SSH client and subsequent ones, and makes password input for them very messy.sudo/doaselevation, attempting to activate with that user innixos apply --target-hostwas broken since it would always ask for a password whenEnsureRemoteRemotePasswordwas called, thus making it non-interactive.This PR solves issues like these by making the root elevation mechanism much more consistent, and by exposing more behavior to the user through settings.
Now, every root command can either choose to:
stdinalong with a newline. The command must support it; i.e.sudocan take passwords onstdin, and this is the default settings.NOPASSWDinsudo, ornopassin doas, etc.Summary by CodeRabbit
New Features
root.commandandroot.password_methodsettingsBug Fixes
Chores
root_commandsetting in favor of new nestedrootconfiguration structure