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

  1. Read the wrapped inner error to identify the real cause
  2. Ensure the user-data `sudo` field is a single line (e.g. `ALL=(ALL) NOPASSWD: ALL`)
  3. Verify the root filesystem is writable and /etc/sudoers.d can be created
  4. 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

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


AI-assisted analysis of lima-vm/lima@dd909d0973 (2026-09-01). Data as JSON: /api/errors/96e6bae7d077106a. Report an issue: GitHub.