lima-vm/lima · error
failed to write sudoers file for user %#q: %w
Error message
failed to write sudoers file for user %#q: %w
What it means
Wraps any failure from writeSudoers while installing the sudoers entry for a user who has a non-empty `sudo` field in user-data. It is a pass-through wrapper: the meaningful cause is inside (newline validation, /etc/sudoers.d creation, open, or write failure). Provisioning of the user is aborted.
Source
Thrown at pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go:312
logrus.Infof("Created user %#q. The password is stored in %#q", u.Name, pwPath)
dotSSHPath := filepath.Join(homedir, ".ssh")
if err = os.MkdirAll(dotSSHPath, 0o700); err != nil {
return fmt.Errorf("failed to create .ssh directory for user %#q: %w", u.Name, err)
}
authKeysPath := filepath.Join(dotSSHPath, "authorized_keys")
authKeysContent := strings.Join(u.SSHAuthorizedKeys, "\n")
if err = os.WriteFile(authKeysPath, []byte(authKeysContent), 0o600); err != nil {
return fmt.Errorf("failed to write authorized_keys file for user %#q: %w", u.Name, err)
}
for _, f := range []string{pwPath, dotSSHPath, authKeysPath} {
if err = os.Chown(f, uid, -1); err != nil {
return fmt.Errorf("failed to chown %#q for user %#q: %w", f, u.Name, err)
}
}
if u.Sudo != "" {
if err := writeSudoers(u.Name, u.Sudo); err != nil {
return fmt.Errorf("failed to write sudoers file for user %#q: %w", u.Name, err)
}
}
return nil
}
// writeSudoers appends a sudoers entry for the given user.
// writeSudoers is expected be called only once on creating the user account.
func writeSudoers(userName, sudo string) error {
if strings.Contains(sudo, "\n") {
return errors.New("sudo field must not contain newline characters")
}
if err := os.MkdirAll("/etc/sudoers.d", 0o700); err != nil {
return fmt.Errorf("failed to create /etc/sudoers.d directory: %w", err)
}
sudoersPath := "/etc/sudoers.d/90-cloud-init-users"
f, err := os.OpenFile(sudoersPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o400)
if err != nil {
return fmt.Errorf("failed to open sudoers file %#q: %w", sudoersPath, err)View on GitHub (pinned to dd909d0973)
Solutions
- Read the wrapped inner error to identify the real cause
- Ensure the user-data `sudo` field is a single line (e.g. `ALL=(ALL) NOPASSWD: ALL`)
- Verify the root filesystem is writable and /etc/sudoers.d can be created
- Remove the `sudo` field if sudo access is not needed, avoiding this code path entirely
Example fix
# before (multi-line sudo value)
users:
- name: alice
sudo: "ALL=(ALL) NOPASSWD: ALL,
ALL=(ALL) NOPASSWD: /usr/bin/ls"
# after
users:
- name: alice
sudo: "ALL=(ALL) NOPASSWD: ALL" Defensive patterns
Strategy: validation
Validate before calling
// validate user-data sudo fields before applying
for _, u := range users {
if u.Sudo != "" && strings.ContainsAny(u.Sudo, "\n\r") {
return fmt.Errorf("user %s: sudo must be a single line", u.Name)
}
} Try / catch
if err := processUserData(ctx, data); err != nil {
if strings.Contains(err.Error(), "sudoers file") {
log.Printf("sudoers provisioning failed: %v — check the wrapped cause", err)
}
} Prevention
- Keep `sudo` values single-line (e.g. ALL=(ALL) NOPASSWD: ALL)
- Avoid YAML block scalars for the sudo field
- Omit the sudo field entirely when sudo access is not needed
When it happens
Trigger: createUser calls writeSudoers(u.Name, u.Sudo) when u.Sudo != "" and writeSudoers returns any error — including 'sudo field must not contain newline characters', '/etc/sudoers.d' mkdir failure, open failure, or write failure.
Common situations: User-data `sudo` value copied from Linux cloud-init examples spanning multiple lines (e.g. nested sudoers rules); read-only root volume; malformed multi-line YAML producing embedded newlines.
Related errors
- failed to enable SSHD: %w
- failed to write password file for user %#q: %w
- failed to write to sudoers file %#q for user %#q: %w
- the file argument can be specified only for --check mode
- unexpected arguments %v
AI-assisted analysis of lima-vm/lima@dd909d0973 (2026-09-01).
Data as JSON: /api/errors/96e6bae7d077106a.
Report an issue: GitHub.