diff --git a/host_exporters/defaults/main.yml b/host_exporters/defaults/main.yml index 1884b5c..66287a4 100644 --- a/host_exporters/defaults/main.yml +++ b/host_exporters/defaults/main.yml @@ -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 . 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 diff --git a/host_exporters/tasks/main.yml b/host_exporters/tasks/main.yml index 3f3d99e..04d3aab 100644 --- a/host_exporters/tasks/main.yml +++ b/host_exporters/tasks/main.yml @@ -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 }}"