host_exporters: IPMI tools read a static root-owned FreeIPMI config pinned in sudoers, sudo logs allowed commands

This commit is contained in:
2026-10-25 15:27:06 +00:00
parent 89821b479b
commit 4dc7b23e06
2 changed files with 44 additions and 7 deletions
+9 -3
View File
@@ -10,10 +10,16 @@ host_exporters_ipmi_collectors:
- bmc
- sel
- chassis
# The tools run as root and parse the FreeIPMI configuration file named by --config-file. The exporter
# writes that file itself (a named pipe in its /tmp, empty for in-band local mode), so a compromised
# exporter could set options such as sdr-cache-directory and make root write anywhere. The wrappers
# therefore replace the path with this static root-owned file, and sudoers admits only that path.
host_exporters_ipmi_freeipmi_conf: /etc/ipmi-exporter/freeipmi.conf
# FreeIPMI tool behind each collector and the fixed arguments the exporter (1.10) passes before
# --config-file <temporary file>. sudo admits exactly these, so a compromised exporter cannot
# run chassis control or clear the SEL. A different exporter version needs matching arguments
# (ipmi_up stays 0 until then). dcmi is not listed: the S1200RP BMC has no power monitoring.
# --config-file. sudo admits exactly these, so a compromised exporter cannot run chassis control or
# clear the SEL. A different exporter version needs matching arguments (ipmi_up stays 0 until then).
# dcmi is not listed: the S1200RP BMC has no power monitoring.
host_exporters_ipmi_tools:
ipmi:
tool: ipmimonitoring
+35 -4
View File
@@ -91,13 +91,34 @@
mode: "0440"
validate: visudo -cf %s
content: |
Defaults:prometheus !log_allowed, !pam_session
Defaults:prometheus !pam_session
{% for collector in host_exporters_ipmi_collectors %}
prometheus ALL = (root) NOPASSWD: /usr/sbin/{{ host_exporters_ipmi_tools[collector].tool }} {{ host_exporters_ipmi_tools[collector].args }} --config-file /tmp/ipmi_exporter-{{ '[0-9a-f]' * 32 }}
prometheus ALL = (root) NOPASSWD: /usr/sbin/{{ host_exporters_ipmi_tools[collector].tool }} {{ host_exporters_ipmi_tools[collector].args }} --config-file {{ host_exporters_ipmi_freeipmi_conf }}
{% endfor %}
register: host_exporters_ipmi_sudo
# collector_cmd replaces the tool itself, so each collector gets a wrapper that adds sudo.
- name: Create the FreeIPMI configuration directory
ansible.builtin.file:
path: "{{ host_exporters_ipmi_freeipmi_conf | dirname }}"
state: directory
owner: root
group: root
mode: "0755"
# See the defaults for why the file is static.
- name: Write the FreeIPMI configuration
ansible.builtin.copy:
dest: "{{ host_exporters_ipmi_freeipmi_conf }}"
owner: root
group: root
mode: "0644"
content: |
# In-band access needs no settings. Not even sdr-cache-directory: the tools without an SDR cache
# reject it, and the cache of the ipmi tool stays in the home of root.
# collector_cmd replaces the tool itself, so each collector gets a wrapper that adds sudo. It swaps
# the exporter's --config-file for the static one. The exporter fills a named pipe at the path it
# passes and waits for a reader; the wrapper drains it so that writer does not hang.
- name: Create the sudo wrappers directory
ansible.builtin.file:
path: /usr/local/libexec/ipmi-exporter
@@ -110,7 +131,17 @@
mode: "0755"
content: |
#!/bin/sh
exec sudo -n /usr/sbin/{{ item.value.tool }} "$@"
n=$#
while [ "$n" -gt 0 ]; do
a=$1; shift; n=$((n - 1))
if [ "$a" = --config-file ] && [ "$n" -gt 0 ]; then
timeout 5 cat "$1" > /dev/null 2>&1
shift; n=$((n - 1))
else
set -- "$@" "$a"
fi
done
exec sudo -n /usr/sbin/{{ item.value.tool }} "$@" --config-file {{ host_exporters_ipmi_freeipmi_conf }}
loop: "{{ host_exporters_ipmi_tools | dict2items | selectattr('key', 'in', host_exporters_ipmi_collectors) | list }}"
loop_control:
label: "{{ item.key }}"