Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/69600.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``saltutil.runner`` and ``saltutil.wheel`` raising ``KeyError: "getpwnam(): name not found: 'sudo_<user>'"`` when an orchestration (``salt-run state.orchestrate``) was launched under ``sudo`` and the rendered SLS called ``salt.saltutil.runner`` from Jinja. ``state.orchestrate`` overwrites ``__opts__["user"]`` with the publishing user (``salt.utils.user.get_specific_user()``, which returns ``"sudo_<login>"`` under ``sudo``), and the post-#67716 privilege-drop path then tried to ``chugid`` to that non-existent account. The privilege-drop helper now validates the candidate against the passwd database and skips the drop when the configured ``user`` is not a real account, falling back to the historical in-process behavior.
1 change: 1 addition & 0 deletions changelog/69833.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``saltutil.runner`` raising ``KeyError: "getpwnam(): name not found: 'Reactor'"`` when a reactor-triggered orchestration rendered an SLS that called ``salt.saltutil.runner`` from Jinja. The reactor stamps ``__user__ = "Reactor"`` on runner/wheel low data (to distinguish reactor-fired events from user-fired ones); ``state.orchestrate`` then copies that sentinel into ``__opts__["user"]``, and the post-#67716 privilege-drop path tried to ``chugid`` to a non-existent ``Reactor`` account. Same underlying fix as #69600: ``_master_user_runas`` now validates the candidate against the passwd database and skips the privilege drop when it does not resolve to a real user.
20 changes: 20 additions & 0 deletions salt/modules/saltutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1952,13 +1952,33 @@ def _master_user_runas(opts):
the Salt master runs as the ``salt`` user by default, so those functions
would otherwise touch master-owned resources (the git_pillar/gitfs cache,
the pki tree, ...) as the wrong user. See #67716.

The ``user`` value in ``opts`` is not always the master's configured
daemon user: ``state.orchestrate`` overwrites ``__opts__['user']`` with
the publishing user (``salt.utils.user.get_specific_user()``), which
returns ``"sudo_<login>"`` when the call was made under ``sudo``. That
is not a real account, so attempting to drop to it would later raise
``KeyError`` from ``pwd.getpwnam`` inside ``chugid``. Validate the
candidate against the passwd database and skip the privilege drop when
it does not resolve to a real user. See #69600.
"""
runas = opts.get("user")
if not runas or runas == salt.utils.user.get_user():
return None
# Changing users requires root; otherwise keep the historical behavior.
if not hasattr(os, "geteuid") or os.geteuid() != 0:
return None
if pwd is not None:
try:
pwd.getpwnam(runas)
except KeyError:
log.debug(
"Not dropping privileges: '%s' is not a real user on this "
"system (likely the publishing user copied into opts by "
"state.orchestrate, e.g. 'sudo_<login>').",
runas,
)
return None
return runas


Expand Down
57 changes: 56 additions & 1 deletion tests/pytests/unit/modules/test_saltutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,12 @@ def cmd(self, name, **kwargs):
({}, 0, "root", None),
),
)
def test_master_user_runas(opts, euid, current_user, expected):
def test_master_user_runas(opts, euid, current_user, expected, monkeypatch):
# The candidate user is validated against the passwd database; stub it
# so the configured ``salt`` user appears to exist on the test host.
monkeypatch.setattr(
saltutil, "pwd", types.SimpleNamespace(getpwnam=lambda user: None)
)
with patch("os.geteuid", return_value=euid), patch(
"salt.utils.user.get_user", return_value=current_user
):
Expand Down Expand Up @@ -401,6 +406,56 @@ def _raise(user):
assert os.environ["HOME"] == "/root"


def test_master_user_runas_unknown_user_returns_none(monkeypatch):
"""
When ``opts['user']`` is not a real account on the system,
``_master_user_runas`` must return ``None`` instead of returning a
name that would later blow up in ``pwd.getpwnam`` inside
``_client_cmd_as`` / ``chugid`` (#69600).

Regression: ``state.orchestrate`` overwrites ``__opts__['user']``
with ``__user__`` (the value of ``salt.utils.user.get_specific_user()``),
which is ``"sudo_<login>"`` whenever ``salt-run`` was launched under
``sudo``. That name has no passwd entry, so attempting to drop to it
raised ``KeyError: "getpwnam(): name not found: 'sudo_<login>'"``
wrapped in ``CommandExecutionError``.
"""

def _raise(user):
raise KeyError(user)

monkeypatch.setattr(saltutil, "pwd", types.SimpleNamespace(getpwnam=_raise))
with patch("os.geteuid", return_value=0), patch(
"salt.utils.user.get_user", return_value="root"
):
assert saltutil._master_user_runas({"user": "sudo_alice"}) is None


def test_master_user_runas_reactor_sentinel_returns_none(monkeypatch):
"""
Same class of bug as #69600 but triggered from the reactor: when the
master (running as root) reacts to an event that runs
``state.orchestrate``, ``salt.utils.reactor.ReactWrap`` stamps
``__user__ = "Reactor"`` on runner/wheel low data so reactor-fired
events can be told apart from user-fired ones.
``state.orchestrate`` copies that value into ``__opts__["user"]``, so
any downstream ``salt['saltutil.runner'](...)`` call from a rendered
SLS reaches ``_master_user_runas`` with ``runas="Reactor"``, which
has no passwd entry. ``_master_user_runas`` must return ``None``
instead of returning ``"Reactor"`` and blowing up later in
``chugid``. See #69833.
"""

def _raise(user):
raise KeyError(user)

monkeypatch.setattr(saltutil, "pwd", types.SimpleNamespace(getpwnam=_raise))
with patch("os.geteuid", return_value=0), patch(
"salt.utils.user.get_user", return_value="root"
):
assert saltutil._master_user_runas({"user": "Reactor"}) is None


def test_align_runas_environment_without_pwd_is_noop(monkeypatch):
"""On platforms without the pwd module (Windows) the helper is a no-op."""
monkeypatch.setattr(saltutil, "pwd", None)
Expand Down
Loading