fix: keep the sudo password out of files written as root (0.2.1)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 25s
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 25s
run_command prepended the sudo password to the caller's stdin. A sudo that does not prompt left it for the command, and fix_snmp's 'cat > snmpd.conf' wrote it as the configuration's first line before making the file world-readable. run_command now refuses stdin to a privileged command while a sudo password is set. fix_snmp stages the configuration in a mktemp file without privilege, and root installs it next to the target and renames it over, so nothing root runs follows a symlink. Closes #8
This commit is contained in:
1 parent
992bc6d5e9
commit
707e048792
4 files changed
+122
-20
No files matched your search
+56
-13
@@ -44,6 +44,25 @@ _SNMPD_CONF = (
|
||||
"sysLocation Managed by netOrk\n"
|
||||
"sysContact netork@localhost\n"
|
||||
)
|
||||
#: Why ``run_command`` refuses stdin to a privileged command while a sudo
|
||||
#: password is set (#8).
|
||||
_PRIVILEGED_STDIN = (
|
||||
"run_command(privileged=True) takes no stdin while a sudo password is set: "
|
||||
"sudo -S reads the password from stdin, and a sudo that does not prompt "
|
||||
"(NOPASSWD, a cached timestamp) leaves it for the command, which would read it "
|
||||
"as the first line of the data. Stage the data without privilege and have root "
|
||||
"install that file."
|
||||
)
|
||||
|
||||
#: Stages content without privilege: a fresh ``mktemp`` file only the login user
|
||||
#: may read, never a fixed name another user could create first. Prints its path.
|
||||
_STAGE_TEMPLATE = "/tmp/napalm-bsd-stage.XXXXXX"
|
||||
_STAGE_PREFIX = _STAGE_TEMPLATE[: -len("XXXXXX")]
|
||||
_STAGE = (
|
||||
f"umask 077 && t=$(mktemp {_STAGE_TEMPLATE}) && "
|
||||
'{ cat > "$t" && printf %s "$t" || { s=$?; rm -f -- "$t"; exit $s; }; }'
|
||||
)
|
||||
|
||||
#: What a working agent answers sysDescr.0 with.
|
||||
_SNMP_TYPES = ("STRING:", "INTEGER:", "OID:", "Timeticks:", "Hex-STRING:", "IpAddress:")
|
||||
|
||||
@@ -155,10 +174,38 @@ class BsdDriver(HostStatusMixin, OSDriver):
|
||||
timeout: float = 60,
|
||||
stdin: Optional[bytes] = None,
|
||||
) -> CommandResult:
|
||||
"""Run *command* on an exec channel: no PTY, stderr apart, a real exit code."""
|
||||
"""Run *command* on an exec channel: no PTY, stderr apart, a real exit code.
|
||||
|
||||
:raises ValueError: for *stdin* to a privileged command while a sudo
|
||||
password is set (see ``_PRIVILEGED_STDIN``).
|
||||
"""
|
||||
line, prefix = self._privileged(command, privileged)
|
||||
data = (prefix or b"") + (stdin or b"") if (prefix or stdin) else None
|
||||
return run_on_transport(self._transport(), line, stdin=data, timeout=timeout)
|
||||
if prefix and stdin is not None:
|
||||
raise ValueError(_PRIVILEGED_STDIN)
|
||||
return run_on_transport(self._transport(), line, stdin=prefix or stdin, timeout=timeout)
|
||||
|
||||
def _install_file_as_root(self, path: str, content: str) -> CommandResult:
|
||||
"""Replace *path* with *content*, as a new file owned by root, mode 644.
|
||||
|
||||
The content is staged without privilege (``_STAGE``), so it never follows
|
||||
a sudo password on stdin. Root then installs the staged file next to *path*
|
||||
and renames it over *path*; nothing root runs follows a symlink at *path*.
|
||||
Nothing runs as root when staging failed.
|
||||
"""
|
||||
staged = self.run_command(_STAGE, timeout=30, stdin=content.encode())
|
||||
lines = staged.stdout.strip().splitlines()
|
||||
tmp = lines[-1] if lines else ""
|
||||
if staged.exit_code != 0 or not tmp.startswith(_STAGE_PREFIX) or "/" in tmp[len(_STAGE_PREFIX):]:
|
||||
reason = (staged.stderr or staged.stdout).strip() or f"could not stage {path}"
|
||||
return CommandResult(reason, "", staged.exit_code or 1)
|
||||
tmp_q, part_q, path_q = (quote(p) for p in (tmp, f"{path}.napalm-tmp", path))
|
||||
directory = quote(path.rsplit("/", 1)[0] or "/")
|
||||
return self.run_command(
|
||||
f"mkdir -p {directory} && install -m 644 -o root -g wheel {tmp_q} {part_q}"
|
||||
f" && mv -f {part_q} {path_q}; s=$?; rm -f {tmp_q} {part_q}; exit $s",
|
||||
privileged=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
def open_stream(self, command: str, *, privileged: bool = False) -> ByteStream:
|
||||
line, prefix = self._privileged(command, privileged)
|
||||
@@ -420,18 +467,14 @@ class BsdDriver(HostStatusMixin, OSDriver):
|
||||
(NetOrk/netork#800). Stops at the first step that fails.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
conf_dir = self.SNMPD_CONF.rsplit("/", 1)[0]
|
||||
steps = (
|
||||
("install", self.INSTALL_COMMAND.format(name="net-snmp"), None),
|
||||
(
|
||||
"config",
|
||||
f"mkdir -p {conf_dir} && cat > {self.SNMPD_CONF} && chmod 644 {self.SNMPD_CONF}",
|
||||
_SNMPD_CONF.encode(),
|
||||
),
|
||||
("service", self.SNMPD_START, None),
|
||||
("install", lambda: self.run_command(
|
||||
self.INSTALL_COMMAND.format(name="net-snmp"), privileged=True, timeout=600)),
|
||||
("config", lambda: self._install_file_as_root(self.SNMPD_CONF, _SNMPD_CONF)),
|
||||
("service", lambda: self.run_command(self.SNMPD_START, privileged=True, timeout=600)),
|
||||
)
|
||||
for label, command, stdin in steps:
|
||||
result = self.run_command(command, privileged=True, timeout=600, stdin=stdin)
|
||||
for label, step in steps:
|
||||
result = step()
|
||||
output = "\n".join(filter(None, (result.stdout.strip(), result.stderr.strip())))
|
||||
lines.append(f"[{label}] {output[-300:]}".rstrip())
|
||||
if result.exit_code != 0:
|
||||
|
||||
Reference in new issue
Block a user