Ensure System Log Files Have Correct Permissions

Classification:

compliance

Framework:

Control:

Description

The file permissions for all log files written by rsyslog should be set to 640, or more restrictive. These log files are determined by the second part of each Rule line in /etc/rsyslog.conf and typically all appear in /var/log. For each log file LOGFILE referenced in /etc/rsyslog.conf, run the following command to inspect the file’s permissions:

$ ls -l *LOGFILE*

If the permissions are not 640 or more restrictive, run the following command to correct this:

$ sudo chmod 640 *LOGFILE*

"

Rationale

Log files can contain valuable information regarding system configuration. If the system log files are not protected unauthorized users could change the logged data, eliminating their forensic value.

Remediation

Shell script

The following script can be run on the host to remediate the issue.

# Remediation is applicable only in certain platforms
if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

# List of log file paths to be inspected for correct permissions
# \* Primarily inspect log file paths listed in /etc/rsyslog.conf
RSYSLOG\_ETC\_CONFIG="/etc/rsyslog.conf"
# \* And also the log file paths listed after rsyslog's $IncludeConfig directive
# (store the result into array for the case there's shell glob used as value of IncludeConfig)
readarray -t OLD\_INC < <(grep -e "\$IncludeConfig[[:space:]]\+[^[:space:];]\+" /etc/rsyslog.conf | cut -d ' ' -f 2)
readarray -t RSYSLOG\_INCLUDE\_CONFIG < <(for INCPATH in "${OLD\_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)
readarray -t NEW\_INC < <(awk '/)/{f=0} /include\(/{f=1} f{nf=gensub("^(include\\(|\\s\*)file=\"(\\S+)\".\*","\\2",1); if($0!=nf){print nf}}' /etc/rsyslog.conf)
readarray -t RSYSLOG\_INCLUDE < <(for INCPATH in "${NEW\_INC[@]}"; do eval printf '%s\\n' "${INCPATH}"; done)

# Declare an array to hold the final list of different log file paths
declare -a LOG\_FILE\_PATHS

# Array to hold all rsyslog config entries
RSYSLOG\_CONFIGS=()
RSYSLOG\_CONFIGS=("${RSYSLOG\_ETC\_CONFIG}" "${RSYSLOG\_INCLUDE\_CONFIG[@]}" "${RSYSLOG\_INCLUDE[@]}")

# Get full list of files to be checked
# RSYSLOG\_CONFIGS may contain globs such as
# /etc/rsyslog.d/\*.conf /etc/rsyslog.d/\*.frule
# So, loop over the entries in RSYSLOG\_CONFIGS and use find to get the list of included files.
RSYSLOG\_CONFIG\_FILES=()
for ENTRY in "${RSYSLOG\_CONFIGS[@]}"
do
 # If directory, rsyslog will search for config files in recursively.
 # However, files in hidden sub-directories or hidden files will be ignored.
 if [ -d "${ENTRY}" ]
 then
 readarray -t FINDOUT < <(find "${ENTRY}" -not -path '\*/.\*' -type f)
 RSYSLOG\_CONFIG\_FILES+=("${FINDOUT[@]}")
 elif [ -f "${ENTRY}" ]
 then
 RSYSLOG\_CONFIG\_FILES+=("${ENTRY}")
 else
 echo "Invalid include object: ${ENTRY}"
 fi
done

# Browse each file selected above as containing paths of log files
# ('/etc/rsyslog.conf' and '/etc/rsyslog.d/\*.conf' in the default configuration)
for LOG\_FILE in "${RSYSLOG\_CONFIG\_FILES[@]}"
do
 # From each of these files extract just particular log file path(s), thus:
 # \* Ignore lines starting with space (' '), comment ('#"), or variable syntax ('$') characters,
 # \* Ignore empty lines,
 # \* Strip quotes and closing brackets from paths.
 # \* Ignore paths that match /dev|/etc.\*\.conf, as those are paths, but likely not log files
 # \* From the remaining valid rows select only fields constituting a log file path
 # Text file column is understood to represent a log file path if and only if all of the
 # following are met:
 # \* it contains at least one slash '/' character,
 # \* it is preceded by space
 # \* it doesn't contain space (' '), colon (':'), and semicolon (';') characters
 # Search log file for path(s) only in case it exists!
 if [[ -f "${LOG\_FILE}" ]]
 then
 NORMALIZED\_CONFIG\_FILE\_LINES=$(sed -e "/^[#|$]/d" "${LOG\_FILE}")
 LINES\_WITH\_PATHS=$(grep '[^/]\*\s\+\S\*/\S\+$' <<< "${NORMALIZED\_CONFIG\_FILE\_LINES}")
 FILTERED\_PATHS=$(awk '{if(NF>=2&&($NF~/^\//||$NF~/^-\//)){sub(/^-\//,"/",$NF);print $NF}}' <<< "${LINES\_WITH\_PATHS}")
 CLEANED\_PATHS=$(sed -e "s/[\"')]//g; /\\/etc.\*\.conf/d; /\\/dev\\//d" <<< "${FILTERED\_PATHS}")
 MATCHED\_ITEMS=$(sed -e "/^$/d" <<< "${CLEANED\_PATHS}")
 # Since above sed command might return more than one item (delimited by newline), split
 # the particular matches entries into new array specific for this log file
 readarray -t ARRAY\_FOR\_LOG\_FILE <<< "$MATCHED\_ITEMS"
 # Concatenate the two arrays - previous content of $LOG\_FILE\_PATHS array with
 # items from newly created array for this log file
 LOG\_FILE\_PATHS+=("${ARRAY\_FOR\_LOG\_FILE[@]}")
 # Delete the temporary array
 unset ARRAY\_FOR\_LOG\_FILE
 fi
done

# Check for RainerScript action log format which might be also multiline so grep regex is a bit
# curly:
# extract possibly multiline action omfile expressions
# extract File="logfile" expression
# match only "logfile" expression
for LOG\_FILE in "${RSYSLOG\_CONFIG\_FILES[@]}"
do
 ACTION\_OMFILE\_LINES=$(grep -ozP "action\s\*\(\s\*type\s\*=\s\*\"omfile\"[^\)]\*\)" "${LOG\_FILE}")
 OMFILE\_LINES=$(echo "${ACTION\_OMFILE\_LINES}"| grep -aoP "File\s\*=\s\*\"([/[:alnum:][:punct:]]\*)\"\s\*\)")
 LOG\_FILE\_PATHS+=("$(echo "${OMFILE\_LINES}"| grep -oE "\"([/[:alnum:][:punct:]]\*)\""|tr -d "\"")")
done

# Ensure the correct attribute if file exists
FILE\_CMD="chmod"
for LOG\_FILE\_PATH in "${LOG\_FILE\_PATHS[@]}"
do
 # Sanity check - if particular $LOG\_FILE\_PATH is empty string, skip it from further processing
 if [ -z "$LOG\_FILE\_PATH" ]
 then
 continue
 fi
 $FILE\_CMD "0640" "$LOG\_FILE\_PATH"
done

else
 >&2 echo 'Remediation is not applicable, nothing was done'
fi

Ansible playbook

The following playbook can be run with Ansible to remediate the issue.

- name: Ensure System Log Files Have Correct Permissions - Set rsyslog logfile configuration
 facts
 ansible.builtin.set\_fact:
 rsyslog\_etc\_config: /etc/rsyslog.conf
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Get IncludeConfig directive
 ansible.builtin.shell: |
 set -o pipefail
 grep -e '$IncludeConfig' {{ rsyslog\_etc\_config }} | cut -d ' ' -f 2 || true
 register: rsyslog\_old\_inc
 changed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Get include files directives
 ansible.builtin.shell: |
 set -o pipefail
 awk '/)/{f=0} /include\(/{f=1} f{nf=gensub("^(include\\(|\\s\*)file=\"(\\S+)\".\*","\\2",1); if($0!=nf){print nf}}' {{ rsyslog\_etc\_config }} || true
 register: rsyslog\_new\_inc
 changed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Aggregate rsyslog includes
 ansible.builtin.set\_fact:
 include\_config\_output: '{{ rsyslog\_old\_inc.stdout\_lines + rsyslog\_new\_inc.stdout\_lines
 }}'
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - List all config files
 ansible.builtin.find:
 paths: '{{ item | dirname }}'
 patterns: '{{ item | basename }}'
 hidden: false
 follow: true
 loop: '{{ include\_config\_output | list + [rsyslog\_etc\_config] }}'
 register: rsyslog\_config\_files
 failed\_when: false
 changed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Extract log files old format
 ansible.builtin.shell: |
 set -o pipefail
 grep -oP '^[^(\s|#|\$)]+[\s]+.\*[\s]+-?(/+[^:;\s]+);\*\.\*$' {{ item.1.path }} | \
 awk '{print $NF}' | \
 sed -e 's/^-//' || true
 loop: '{{ rsyslog\_config\_files.results | subelements(''files'') }}'
 register: log\_files\_old
 changed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Extract log files new format
 ansible.builtin.shell: |
 set -o pipefail
 grep -ozP "action\s\*\(\s\*type\s\*=\s\*\"omfile\"[^\)]\*\)" {{ item.1.path }} | \
 grep -aoP "File\s\*=\s\*\"([/[:alnum:][:punct:]]\*)\"\s\*\)" | \
 grep -oE "\"([/[:alnum:][:punct:]]\*)\"" | \
 tr -d "\""|| true
 loop: '{{ rsyslog\_config\_files.results | subelements(''files'') }}'
 register: log\_files\_new
 changed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions - Sum all log files found
 ansible.builtin.set\_fact:
 log\_files: '{{ log\_files\_new.results | map(attribute=''stdout\_lines'') | list
 | flatten | unique + log\_files\_old.results | map(attribute=''stdout\_lines'')
 | list | flatten | unique }}'
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions

- name: Ensure System Log Files Have Correct Permissions -Setup log files attribute
 ansible.builtin.file:
 path: '{{ item }}'
 mode: '0640'
 state: file
 loop: '{{ log\_files | list | flatten | unique }}'
 failed\_when: false
 when: ansible\_virtualization\_type not in ["docker", "lxc", "openvz", "podman", "container"]
 tags:
 - CCE-80191-0
 - NIST-800-53-AC-6(1)
 - NIST-800-53-CM-6(a)
 - PCI-DSS-Req-10.5.1
 - PCI-DSS-Req-10.5.2
 - PCI-DSSv4-10.3.1
 - PCI-DSSv4-10.3.2
 - configure\_strategy
 - low\_complexity
 - medium\_disruption
 - medium\_severity
 - no\_reboot\_needed
 - rsyslog\_files\_permissions