CIS Hardening of a Debian Linux Server Part 6: Level 1 Logging and Auditing

This is the seventh entry in the series documenting the Debian 13 server CIS hardening project.

This entry covers sections 6.1 through 6.3 of the benchmark. This is a Level 1 pass. Level 2 recommendations are deferred.

6.1 System Logging #

This section covers 6.1.1 through 6.1.3 of the benchmark.

systemd-journald and rsyslog can both be used on Debian 13, and the benchmark does not force a single approach. The setup adopted here keeps journald as the primary log collector, since it is the systemd default, and uses rsyslog as the log processing, storage, and forwarding layer. This preserves the systemd default behavior while retaining traditional log files and rsyslog’s forwarding capabilities.

6.1.1 Configure journald #

This section covers 6.1.1.1 through 6.1.1.2 of the benchmark. The second subsection, covering systemd-journal-remote, only applies when journald is the chosen method for client-side logging. Since rsyslog is used for that purpose here, the recommendations in that subsection do not apply.

6.1.1.1 Configure systemd-journald Service #

6.1.1.1.1 Ensure journald Service Is Active #
# systemctl is-active systemd-journald.service
active

Passes.

6.1.1.1.2 Ensure journald Log File Access Is Configured #
#!/usr/bin/env bash
{
  config_files=(/etc/tmpfiles.d/systemd.conf /usr/lib/tmpfiles.d/systemd.conf)
  warn=()
  for file in "${config_files[@]}"; do
    [ -f "$file" ] || continue
    while read -r type path perm _; do
      if [[ "$type" == "f" && "$perm" =~ ^0?[0-7]{3,4}$ ]]; then
        perm="${perm#0}"
        if [ -f "$path" ]; then
          actual=$(stat -c "%a" "$path" 2>/dev/null)
          if [[ -n "$actual" && "$actual" -gt 640 ]]; then
            warn+=("File: $path | Perm: $actual (expected max: 640) | Defined in: $file")
          fi
        fi
      fi
    done < "$file"
  done
  if [ "${#warn[@]}" -eq 0 ]; then
    echo "No files with permissions more permissive than 0640."
  else
    echo -e " *** REVIEW ***"
    printf "%s\n" "${warn[@]}"
  fi
}
No files with permissions more permissive than 0640.

Passes. Both configuration files are taken into account by the script.

6.1.1.1.3 Ensure journald Log File Rotation Is Configured #
# systemd-analyze cat-config systemd/journald.conf | tac | grep -Psi -- '\b(SystemMaxUse|SystemKeepFree|RuntimeMaxUse|RuntimeKeepFree|MaxFileSec)='
#MaxFileSec=1month
#RuntimeKeepFree=
#RuntimeMaxUse=
#SystemKeepFree=
#SystemMaxUse=

All relevant options are commented out, so the recommendation fails as is. man journald.conf clarifies that the System* options apply to /var/log/journal when persistent storage is used, and the Runtime* options apply to the in-memory journal at /run/log/journal. Absent explicit configuration, SystemMaxUse and RuntimeMaxUse default to 10 percent of the respective filesystem’s size, SystemKeepFree and RuntimeKeepFree default to 15 percent, each capped at 4 GB, and MaxFileSec defaults to one month.

There is no reason to deviate from these defaults. The values are nonetheless set explicitly in journald.conf, as the benchmark recommends, for predictability, auditability, and stability benefits.

Filesystem sizes, from df -h:

/dev/sda1        38G  1.5G   35G   5% /
tmpfs           383M  560K  383M   1% /run

Calculated values:

SystemMaxUse=min(38G*0.10, 4G)=3.8G
SystemKeepFree=min(38G*0.15, 4G)=4G
RuntimeMaxUse=min(383M*0.10, 4G)=38M
RuntimeKeepFree=min(383M*0.15, 4G)=57M
MaxFileSec=1month

Set in /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=3.8G
SystemKeepFree=4G
RuntimeMaxUse=38M
RuntimeKeepFree=57M
MaxFileSec=1month
# systemd-analyze cat-config systemd/journald.conf | tac | grep -Psi -- '\b(SystemMaxUse|SystemKeepFree|RuntimeMaxUse|RuntimeKeepFree|MaxFileSec)='
MaxFileSec=1month
RuntimeKeepFree=57M
RuntimeMaxUse=38M
SystemKeepFree=4G
SystemMaxUse=3.8G

Passes.

This is revisited after the repartitioning work done in 1.1.2. /var/log is now at a standalone partition with 6G of space. The values for SystemMaxUse and SystemKeepFree are recalculated:

SystemMaxUse=min(6G*0.10, 4G)=600M
SystemKeepFree=min(6G*0.15, 4G)=900M

The audit is run again after the values in /etc/systemd/journald.conf have been replaced:

# systemd-analyze cat-config systemd/journald.conf | tac | grep -Psi -- '\b(SystemMaxUse|SystemKeepFree|RuntimeMaxUse|RuntimeKeepFree|MaxFileSec)='
MaxFileSec=1month
RuntimeKeepFree=57M
RuntimeMaxUse=38M
SystemKeepFree=900M
SystemMaxUse=600M

Still passes.

6.1.1.1.4 Ensure journald ForwardToSyslog Is Disabled #

Per the benchmark’s note, this recommendation only applies when journald is the chosen method for client-side logging. It is therefore not implemented.

6.1.1.1.5 Ensure journald Storage Is Configured #
#!/usr/bin/env bash
{
  l_analyze_cmd="$(readlink -e /bin/systemd-analyze || \
  readlink -e /usr/bin/systemd-analyze)"
  l_conf_file="systemd/journald.conf" l_block="Journal"
  l_option="Storage" l_option_value="persistent" a_output=()
  while IFS= read -r l_file; do
    l_file="${l_file//# /}"
    l_opt="$(awk '/\['"$l_block"'\]/{a=1;next}/\[/{a=0}a' "$l_file" \
    2>/dev/null | grep -Poi '^\h*'"$l_option"'\h*=\h*\H+\b' | tail -n 1)"
    l_option_value="$(cut -d= -f2 <<< "$l_opt" | xargs)"
    [ -n "$l_option_value" ] && \
    a_output+=(" - \"$l_option\" is set to: \"$l_option_value\"" \
    " in: \"$l_file\"")
  done < <("$l_analyze_cmd" cat-config "$l_conf_file" | tac | \
  grep -Pio '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
  if [ "${#a_output[@]}" -le "0" ]; then
    l_file="$(readlink -e /etc/"$l_conf_file" || \
    readlink -e /usr/lib/"$l_conf_file")"
    l_opt="$(awk '/\['"$l_block"'\]/{a=1;next}/\[/{a=0}a' "$l_file" \
    2>/dev/null | grep -Poim 1 '^(\h*#)?\h*'"$l_option"'\h*=\h*\H+\b')"
    l_option_value="$(cut -d= -f2 <<< "${l_opt//# /}" | xargs)"
    [ -n "$l_option_value" ] && \
    a_output+=(" - The default value: \"${l_opt//#/}\"" \
    " is being used in the configuration")
  fi
  [ "${#a_output[@]}" -gt "0" ] && printf '%s\n' "" "${a_output[@]}" ""
}
 - The default value: "Storage=auto"
 is being used in the configuration

Fails. The recommended value is persistent. Drop-in configuration files under *.conf.d/ take precedence over the main configuration file, with later files in lexicographic order overriding earlier ones for single-value options. Since prior recommendations in this section were already applied directly in /etc/systemd/journald.conf, the same file is used here for consistency.

The Storage line is uncommented and set to persistent in /etc/systemd/journald.conf, and the service is reloaded:

# systemctl reload-or-restart systemd-journald
 - "Storage" is set to: "persistent"
 in: "/etc/systemd/journald.conf"

Passes.

6.1.1.1.6 Ensure journald Compress Is Configured #

The audit for Compress returns:

 - The default value: "Compress=yes"
 is being used in the configuration

Passes. The default already satisfies the recommendation.

6.1.1.2 Configure systemd-journal-remote #

As noted above, this subsection only applies when journald is the chosen method for client-side logging, which is not the case here. Recommendations 6.1.1.2.1 through 6.1.1.2.4 are therefore skipped. A check confirms the package is absent regardless:

# dpkg-query -s systemd-journal-remote &>/dev/null && echo "systemd-journal-remote is installed"

No output. The package is not installed.

6.1.2 Configure rsyslog #

This section covers 6.1.2.1 through 6.1.2.11 of the benchmark. Recommendations 6.1.2.9 through 6.1.2.11 are Level 2 and are deferred to a future pass.

6.1.2.1 Ensure rsyslog Is Installed #

# dpkg-query -s rsyslog &>/dev/null && echo "rsyslog is installed"

No output. rsyslog is not installed.

# apt install rsyslog

Installs rsyslog along with its dependencies (libestr0, libfastjson4, liblognorm5).

# dpkg-query -s rsyslog &>/dev/null && echo "rsyslog is installed"
rsyslog is installed

Passes.

6.1.2.2 Ensure rsyslog Service Is Enabled and Active #

# systemctl is-enabled rsyslog
enabled
# systemctl is-active rsyslog.service
active

Passes.

6.1.2.3 Ensure journald Is Configured to Send Logs to rsyslog #

The audit for ForwardToSyslog returns:

 - "ForwardToSyslog" is set to: "yes"
 in: "/usr/lib/systemd/journald.conf.d/syslog.conf"

Passes. This is set by a vendor-provided drop-in file rather than requiring manual configuration.

# systemctl list-units --type service | grep -P -- '(journald|rsyslog)'
  rsyslog.service                                       loaded active running System Logging Service
  systemd-journald.service                              loaded active running Journal Service

Both services are loaded and active.

6.1.2.4 Ensure rsyslog Log File Creation Mode Is Configured #

# grep -Ps '^\h*\$FileCreateMode\h+0[0,2,4,6][0,2,4]0\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf
/etc/rsyslog.conf:$FileCreateMode 0640

Passes.

6.1.2.5 Ensure rsyslog Logging Is Configured #

This is a manual recommendation. The benchmark’s example configuration for this recommendation references files such as /var/log/secure and /var/log/messages, which belong to the RHEL logging convention rather than Debian’s. This is another instance, consistent with earlier sections of this project, of RHEL-oriented content found in the benchmark’s Debian edition (see Level 1 Access Control).

Reviewing /etc/rsyslog.conf and the drop-in files under /etc/rsyslog.d/ shows the default Debian rule set in place: a catch-all rule sending everything except private authentication messages to /var/log/syslog, dedicated rules for auth, cron, kern, mail, and user facilities, and an emergency broadcast rule. A single drop-in, 20-ufw.conf, routes UFW-tagged messages to /var/log/ufw.log.

Rsyslog’s basic rule syntax splits each line into a selector field and an action field. The selector field combines a facility subfield (auth, kern, mail, and so on, or * for all facilities) and a priority subfield (debug, info, notice, and so on, ascending in severity, with * for all priorities), separated by a dot. Multiple selectors can be chained with semicolons, with later entries overriding earlier ones for the same facility. The action field specifies a destination, which can be a file, a user, a remote host, a named pipe, or a terminal.

No existing rule captures messages of warning severity or higher into a single dedicated file. A rule is added to /etc/rsyslog.conf for this purpose:

*.warning                       /var/log/warnings.log
# systemctl restart rsyslog

The file is not created until a matching message is actually logged. Two test messages are sent at different facilities and priorities:

# logger -p daemon.warning "test warning and above catch-all"
# logger -p user.crit "test warning and above catch-all"
# ls -l /var/log | grep "warning"
-rw-r-----  1 root adm                 80 Jul 13 01:37 warnings.log
# cat /var/log/warnings.log
2026-07-13T01:37:07.507530+00:00 starter root: test warning and above catch-all
2026-07-13T01:42:15.783912+00:00 starter root: test warning and above catch-all

Both messages are captured, and the file inherits the permissions set globally at the top of rsyslog.conf (0640, owner root, group adm). The log entries do not include facility or priority information; rsyslog templates could add this, but this is left out of scope for this pass.

6.1.2.6 Ensure rsyslog Is Configured to Send Logs to a Remote Log Host #

# grep "^*.*[^I][^I]*@" /etc/rsyslog.conf /etc/rsyslog.d/*.conf
# grep -Psi -- '^\s*([^#]+\s+)?action\(([^#]+\s+)?\btarget=\"?[^#"]+\"?\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf

No output for either check. No remote forwarding is currently configured. No remote log host exists in this environment at this stage, so this recommendation is not implemented. Implementation is deferred until a dedicated log collector is provisioned.

A dedicated log host was later provisioned and configured to receive forwarded logs over an encrypted, mutually authenticated channel. Full implementation detail, including certificate authority setup and rsyslog configuration on both ends, is documented under Level 2 Logging and Auditing, which covers the corresponding recommendations 6.1.2.10 and 6.1.2.11.

# grep "^*.*[^I][^I]*@" /etc/rsyslog.conf /etc/rsyslog.d/*.conf
# grep -Psi -- '^\s*([^#]+\s+)?action\(([^#]+\s+)?\btarget=\"?[^#"]+\"?\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf

Both checks still return no output, despite remote forwarding being active and verified. The first check targets the legacy *.* @host forwarding syntax, which does not apply here since the configuration uses the modern action() block syntax instead. The second check fails for a different reason: grep matches within a single line by default, and the pattern requires action( and target= to appear on the same line. The configuration in use, consistent with the formatting shown throughout rsyslog’s own documentation, spreads an action() block’s parameters across multiple lines for readability, so target= never appears on the same line as action(. The recommendation is satisfied; the audit script’s regex does not account for multi-line action() blocks.

6.1.2.7 Ensure rsyslog Is Not Configured to Receive Logs from a Remote Client #

# grep -Psi -- '^\h*module\(load=\"?imtcp\"?\)' /etc/rsyslog.conf /etc/rsyslog.d/*.conf
# grep -Psi -- '^\h*input\(type=\"?imtcp\"?\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf
# grep -Psi -- '^\h*\$ModLoad\h+imtcp\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf
# grep -Psi -- '^\h*\$InputTCPServerRun\b' /etc/rsyslog.conf /etc/rsyslog.d/*.conf

No output for any of the four checks. Passes.

6.1.2.8 Ensure logrotate Is Configured #

This is a manual recommendation. /etc/logrotate.conf sets weekly rotation, four retained backlogs, and creation of new files after rotation, with per-package overrides supplied through /etc/logrotate.d/. Existing package-provided configurations cover alternatives, apt, aptitude, dpkg, rsyslog, ufw, unattended-upgrades, wtmp, wtmpdb, and locally maintained entries for btmp and wtmp, each with rotation cadences ranging from weekly to yearly.

There is no reason to deviate from the existing per-file rotation settings. The one required change is adding the newly created /var/log/warnings.log to the rsyslog rotation stanza, alongside the other rsyslog-managed logs, in /etc/logrotate.d/rsyslog:

/var/log/syslog
/var/log/mail.log
/var/log/kern.log
/var/log/auth.log
/var/log/user.log
/var/log/cron.log
/var/log/warnings.log
{
        rotate 4
        weekly
        missingok
        notifempty
        compress
        delaycompress
        sharedscripts
        postrotate
                /usr/lib/rsyslog/rsyslog-rotate
        endscript
}
# logrotate -d /etc/logrotate.d/rsyslog

Debug output shows no errors. warnings.log is picked up and correctly reported as not yet due for rotation.

The benchmark additionally recommends setting maxage as a safety net, so that rotated log files are still removed on a bound schedule even if a rotation cycle is interrupted and fails to delete them, without overriding the standard rotation cadence itself. A global maxage in logrotate.conf is not practical here, since the existing per-file cadences vary from weekly to yearly. Instead, maxage is set individually per file, calculated at slightly above the number of days each file’s own rotate and cadence settings would normally take to age out:

alternatives: 12 * 31 ~ 400
apt: 12 * 31 ~ 400
aptitude: 6 * 31 ~ 200
btmp: 1 * 31 ~ 40
dpkg: 12 * 31 ~ 400
rsyslog: 4 * 7 ~ 40
ufw: 4 * 7 ~ 40
unattended-upgrades: 6 * 31 ~ 200
wtmp: 1 * 31 ~ 40
wtmpdb: 4 * 366 ~ 1600

The cloud-init configuration uses a size-based rather than time-based cadence and is left unchanged. maxage <n> is added inside each remaining stanza:

# grep maxage /etc/logrotate.d/*
/etc/logrotate.d/alternatives:  maxage 400
/etc/logrotate.d/apt:  maxage 400
/etc/logrotate.d/aptitude:  maxage 200
/etc/logrotate.d/btmp:    maxage 40
/etc/logrotate.d/dpkg:  maxage 400
/etc/logrotate.d/rsyslog:       maxage 40
/etc/logrotate.d/ufw:   maxage 40
/etc/logrotate.d/unattended-upgrades:  maxage 200
/etc/logrotate.d/wtmp:    maxage 40
/etc/logrotate.d/wtmpdb:    maxage 1600

logrotate -d is re-run against each modified file after the change, with no errors reported in any case.

6.1.2.9 Ensure rsyslog-gnutls Is Installed #

Level 2, deferred.

6.1.2.10 Ensure rsyslog Forwarding Uses TLS #

Level 2, deferred.

6.1.2.11 Ensure rsyslog CA Certificates Are Configured #

Level 2, deferred.

6.1.3 Configure Logfiles #

This section covers 6.1.3.1 of the benchmark.

6.1.3.1 Ensure Access to All Logfiles Has Been Configured #

#!/usr/bin/env bash
{
  a_output=(); a_output2=()
  f_file_test_chk()
  {
    a_out2=()
    maxperm="$( printf '%o' $(( 0777 & ~$perm_mask)) )"
    [ $(( $l_mode & $perm_mask )) -gt 0 ] && \
    a_out2+=(" o Mode: \"$l_mode\" should be \"$maxperm\" or more restrictive")
    [[ ! "$l_user" =~ $l_auser ]] && \
    a_out2+=(" o Owned by: \"$l_user\" and should be owned by \"${l_auser//|/ or }\"")
    [[ ! "$l_group" =~ $l_agroup ]] && \
    a_out2+=(" o Group owned by: \"$l_group\" and should be group owned by \"${l_agroup//|/ or }\"")
    [ "${#a_out2[@]}" -gt 0 ] && a_output2+=(" - File: \"$l_fname\" is:" "${a_out2[@]}")
  }
  while IFS= read -r -d $'\0' l_file; do
    while IFS=: read -r l_fname l_mode l_user l_group; do
      if grep -Pq -- '\/(apt)\h*$' <<< "$(dirname "$l_fname")"; then
        perm_mask='0133' l_auser="root" l_agroup="(root|adm)"; f_file_test_chk
      else
        case "$(basename "$l_fname")" in
          lastlog | lastlog.* | wtmp | wtmp.* | wtmp-* | btmp | btmp.* | btmp-* | README)
            perm_mask='0113' l_auser="root" l_agroup="(root|utmp)"
            f_file_test_chk ;;
          cloud-init.log* | localmessages* | waagent.log*)
            perm_mask='0133' l_auser="(root|syslog)" l_agroup="(root|adm)"
            file_test_chk ;;
          secure{,*.*,.*,-*} | auth.log | syslog | messages)
            perm_mask='0137' l_auser="(root|syslog)" l_agroup="(root|adm)"
            f_file_test_chk ;;
          *.journal | *.journal~)
            perm_mask='0137' l_auser="root" l_agroup="(root|systemd-journal)"
            f_file_test_chk ;;
          *)
            perm_mask='0137' l_auser="(root|syslog)" l_agroup="(root|adm)"
            if [ "$l_user" = "root" ] || ! grep -Pq -- "^\h*$(awk -F: '$1=="'"$l_user"'" {print $7}' /etc/passwd)\b" /etc/shells; then
              ! grep -Pq -- "$l_auser" <<< "$l_user" && l_auser="(root|syslog|$l_user)"
              ! grep -Pq -- "$l_agroup" <<< "$l_group" && l_agroup="(root|adm|$l_group)"
            fi
            f_file_test_chk ;;
        esac
      fi
    done < <(stat -Lc '%n:%#a:%U:%G' "$l_file")
  done < <(find -L /var/log -type f \( -perm /0137 -o ! -user root -o ! -group root \) -print0)
  if [ "${#a_output2[@]}" -le 0 ]; then
    a_output+=(" - All files in \"/var/log/\" have appropriate permissions and ownership")
    printf '\n%s' "- Audit Result:" " ** PASS **" "${a_output[@]}" ""
  else
    printf '\n%s' "- Audit Result:" " ** FAIL **" " - Reason(s) for audit failure:" "${a_output2[@]}" ""
  fi
}
-bash: file_test_chk: command not found

- Audit Result:
 ** FAIL **
 - Reason(s) for audit failure:
 - File: "/var/log/alternatives.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/dpkg.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/sudo.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/alternatives.log.1" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades-dpkg.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades-shutdown.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades-dpkg.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades-shutdown.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades.log" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/unattended-upgrades/unattended-upgrades.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 - File: "/var/log/dpkg.log.1" is:
 o Mode: "0644" should be "640" or more restrictive

Fails. Eleven files under /var/log carry mode 0644 where 0640 or more restrictive is expected. A bash: file_test_chk: command not found line also appears ahead of the result. This traces to a typo in the benchmark’s published script: the function defined earlier in the script is f_file_test_chk, but the cloud-init.log* | localmessages* | waagent.log* case branch calls file_test_chk, missing the leading f_. As a result, the permissions and ownership check is silently skipped for any file the branch matches.

The benchmark’s remediation script carries the same typo, in its f_file_test_fix counterpart:

#!/usr/bin/env bash
{
  a_output2=()
  f_file_test_fix()
  {
    a_out2=()
    maxperm="$( printf '%o' $(( 0777 & ~$perm_mask)) )"
    if [ $(( $l_mode & $perm_mask )) -gt 0 ]; then
      a_out2+=(" o Mode: \"$l_mode\" should be \"$maxperm\" or more restrictive" " x Removing excess permissions")
      chmod "$l_rperms" "$l_fname"
    fi
    if [[ ! "$l_user" =~ $l_auser ]]; then
      a_out2+=(" o Owned by: \"$l_user\" and should be owned by \"${l_auser//|/ or }\"" " x Changing ownership to: \"$l_fix_account\"")
      chown "$l_fix_account" "$l_fname"
    fi
    if [[ ! "$l_group" =~ $l_agroup ]]; then
      a_out2+=(" o Group owned by: \"$l_group\" and should be group owned by \"${l_agroup//|/ or }\"" " x Changing group ownership to: \"$l_fix_account\"")
      chgrp "$l_fix_account" "$l_fname"
    fi
    [ "${#a_out2[@]}" -gt 0 ] && a_output2+=(" - File: \"$l_fname\" is:" "${a_out2[@]}")
  }
  l_fix_account='root'
  while IFS= read -r -d $'\0' l_file; do
    while IFS=: read -r l_fname l_mode l_user l_group; do
      if grep -Pq -- '\/(apt)\h*$' <<< "$(dirname "$l_fname")"; then
        perm_mask='0133' l_rperms="u-x,go-wx" l_auser="root" l_agroup="(root|adm)"; f_file_test_fix
      else
        case "$(basename "$l_fname")" in
          lastlog | lastlog.* | wtmp | wtmp.* | wtmp-* | btmp | btmp.* | btmp-* | README)
            perm_mask='0113' l_rperms="ug-x,o-wx" l_auser="root" l_agroup="(root|utmp)"
            f_file_test_fix ;;
          cloud-init.log* | localmessages* | waagent.log*)
            perm_mask='0133' l_rperms="u-x,go-wx" l_auser="(root|syslog)" l_agroup="(root|adm)"
            file_test_fix ;;
          secure | auth.log | syslog | messages)
            perm_mask='0137' l_rperms="u-x,g-wx,o-rwx" l_auser="(root|syslog)" l_agroup="(root|adm)"
            f_file_test_fix ;;
          *.journal | *.journal~)
            perm_mask='0137' l_rperms="u-x,g-wx,o-rwx" l_auser="root" l_agroup="(root|systemd-journal)"
            f_file_test_fix ;;
          *)
            perm_mask='0137' l_rperms="u-x,g-wx,o-rwx" l_auser="(root|syslog)" l_agroup="(root|adm)"
            if [ "$l_user" = "root" ] || ! grep -Pq -- "^\h*$(awk -F: '$1=="'"$l_user"'" {print $7}' /etc/passwd)\b" /etc/shells; then
              ! grep -Pq -- "$l_auser" <<< "$l_user" && l_auser="(root|syslog|$l_user)"
              ! grep -Pq -- "$l_agroup" <<< "$l_group" && l_agroup="(root|adm|$l_group)"
            fi
            f_file_test_fix ;;
        esac
      fi
    done < <(stat -Lc '%n:%#a:%U:%G' "$l_file")
  done < <(find -L /var/log -type f \( -perm /0137 -o ! -user root -o ! -group root \) -print0)
  if [ "${#a_output2[@]}" -le 0 ]; then
    a_output+=(" - All files in \"/var/log/\" have appropriate permissions and ownership")
    printf '\n%s' "- All files in \"/var/log/\" have appropriate permissions and ownership" " o No changes required" ""
  else
    printf '\n%s' "${a_output2[@]}" ""
  fi
}
-bash: file_test_fix: command not found

 - File: "/var/log/alternatives.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/dpkg.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/sudo.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/alternatives.log.1" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades-dpkg.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades-shutdown.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades-dpkg.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades-shutdown.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades.log" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/unattended-upgrades/unattended-upgrades.log.1.gz" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions
 - File: "/var/log/dpkg.log.1" is:
 o Mode: "0644" should be "640" or more restrictive
 x Removing excess permissions

Permissions are corrected for all eleven flagged files. The audit script is re-run, unmodified, before making any further changes:

-bash: file_test_chk: command not found

- Audit Result:
 ** PASS **
 - All files in "/var/log/" have appropriate permissions and ownership

The result now passes, but the same file_test_chk: command not found line still appears, coming from the same cloud-init.log* branch. A passing result produced by a script with a broken branch is not trustworthy for the files that branch is meant to cover, so the typo is corrected in both scripts before treating this recommendation as satisfied. In each script, the call inside the cloud-init.log* | localmessages* | waagent.log* case is changed from file_test_chk to f_file_test_chk in the audit script, and from file_test_fix to f_file_test_fix in the remediation script.

The corrected audit script is run again:

- Audit Result:
 ** PASS **
 - All files in "/var/log/" have appropriate permissions and ownership

The error is gone, and the result still passes, this time with the cloud-init.log* | localmessages* | waagent.log* branch actually exercised.

Correcting file permissions directly does not address the root cause: whatever process or configuration originally created a file with permissive defaults keeps doing so on every rotation unless that configuration is changed as well. The eleven flagged files trace back to three logrotate configurations, alternatives, dpkg, and unattended-upgrades, plus sudo.log, which had no logrotate configuration of its own.

For alternatives and dpkg, the existing create 644 root root line is changed to create 640 root root in /etc/logrotate.d/alternatives and /etc/logrotate.d/dpkg respectively.

sudo.log has no corresponding file under /etc/logrotate.d:

# ls /etc/logrotate.d
alternatives  apt  aptitude  btmp  cloud-init  dpkg  rsyslog  ufw  unattended-upgrades  wtmp  wtmpdb

A new configuration is created:

# cat /etc/logrotate.d/sudo
/var/log/sudo.log
{
        create 640 root root
        rotate 6
        monthly
        missingok
        notifempty
        compress
        delaycompress
        maxage 200
}

The existing unattended-upgrades configuration has no create line at all:

# cat /etc/logrotate.d/unattended-upgrades
/var/log/unattended-upgrades/unattended-upgrades.log
/var/log/unattended-upgrades/unattended-upgrades-dpkg.log
/var/log/unattended-upgrades/unattended-upgrades-shutdown.log
{
  rotate 6
  monthly
  compress
  missingok
  notifempty
  maxage 200
}

Without an explicit create line, file creation falls back to the bare create directive in logrotate.conf, which creates new files without altering the permissions carried over from the original. This is sufficient on its own, but an explicit create 640 is added regardless as a safeguard: without it, if a file’s permissions were ever changed by mistake and left uncorrected, the implicit fallback would keep recreating it with the wrong mode indefinitely, whereas an explicit line keeps the intended mode enforced on every rotation.

The three files covered by this configuration do not share the same group ownership:

# ls -l /var/log/unattended-upgrades/
total 56
-rw-r----- 1 root adm  15500 Jul 12 06:36 unattended-upgrades-dpkg.log
-rw-r----- 1 root adm    726 Jul  6 06:53 unattended-upgrades-dpkg.log.1.gz
-rw-r----- 1 root root 21953 Jul 13 08:36 unattended-upgrades.log
-rw-r----- 1 root root   601 Jun 30 14:50 unattended-upgrades.log.1.gz
-rw-r----- 1 root root     0 Jul  1 00:28 unattended-upgrades-shutdown.log
-rw-r----- 1 root root   256 Jun 29 22:18 unattended-upgrades-shutdown.log.1.gz

The configuration is split into two stanzas accordingly, one for the root-group files and one for the adm-group unattended-upgrades-dpkg.log:

# cat /etc/logrotate.d/unattended-upgrades
/var/log/unattended-upgrades/unattended-upgrades.log
/var/log/unattended-upgrades/unattended-upgrades-shutdown.log
{
  rotate 6
  monthly
  compress
  missingok
  notifempty
  maxage 200
  create 640 root root
}

/var/log/unattended-upgrades/unattended-upgrades-dpkg.log
{
  rotate 6
  monthly
  compress
  missingok
  notifempty
  maxage 200
  create 640 root adm
}

Both stanzas are checked with logrotate -d, with no errors reported.

A Lynis scan is run at the end of this section:

================================================================================

  Lynis security scan details:

  Scan mode:
  Normal [▆]  Forensics [ ]  Integration [ ]  Pentest [ ]

  Lynis modules:
  - Compliance status      [?]
  - Security audit         [V]
  - Vulnerability scan     [V]

  Details:
  Hardening index : 72 [##############      ]
  Tests performed : 275
  Plugins enabled : 2

  Software components:
  - Firewall               [V]
  - Intrusion software     [X]
  - Malware scanner        [X]

  Files:
  - Test and debug information      : /var/log/lynis.log
  - Report data                     : /var/log/lynis-report.dat

================================================================================

The hardening index remains at 72, unchanged from the previous entry in this series. None of the journald configuration, rsyslog installation, log file permission corrections, or logrotate adjustments carried out across 6.1 move the score. This is worth keeping in mind heading into the next section, where AIDE is introduced with comparatively little configuration by comparison.

6.2 System Auditing #

This section covers 6.2.1 through 6.2.4 of the benchmark. All recommendations in this section are Level 2 and are deferred to a future pass.

6.3 Configure Integrity Checking #

This section covers 6.3.1 through 6.3.3 of the benchmark.

AIDE, the Advanced Intrusion Detection Environment, is a host-based intrusion detection tool. It builds a baseline database of file attributes and cryptographic checksums, then flags any deviation from that baseline on subsequent runs. Further detail is available in aide.conf(5), aide(1), and AIDE’s own documentation at aide.github.io/doc.

6.3.1 Ensure AIDE Is Installed #

# dpkg-query -s aide &>/dev/null && echo "aide is installed"
# dpkg-query -s aide-common &>/dev/null && echo "aide-common is installed"

No output for either. Neither package is installed.

# apt install aide aide-common

aide and aide-common, along with the liblockfile1 dependency are installed.

# dpkg-query -s aide &>/dev/null && echo "aide is installed"
aide is installed
# dpkg-query -s aide-common &>/dev/null && echo "aide-common is installed"
aide-common is installed

The benchmark’s own guidance here defers to the administrator, recommending that AIDE be configured as appropriate for the environment and that its documentation be consulted for available options, rather than specifying an exact rule set.

aide-common ships a large set of pre-built rule fragments under /etc/aide/aide.conf.d, one per common package, so that installing AIDE alongside a given service (rsyslog, cron, ufw, and so on) typically already comes with reasonable tracking rules for that service’s files, without requiring them to be written from scratch. Some of these fragments are executable shell scripts rather than static rule files, run at configuration-load time if their executable bit is set.

AIDE’s own log_level and report_level settings, visible commented out near the top of /etc/aide/aide.conf, control the verbosity of AIDE’s own diagnostic output and report formatting. Despite the similar-looking names, they are unrelated to the facility and priority scale used by rsyslog in 6.1.2.5.

Coverage of top-level paths is confirmed by checking each entry under / against AIDE’s own --list output. Every directory is covered except /proc, /sys, and lost+found, none of which need tracking.

With the packages installed and the default configuration otherwise untouched, the baseline database is generated:

# aideinit
# ls /var/lib/aide
aide.db  aide.db.new

The new database is promoted into place:

# mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

An initial check is run against this baseline:

# aide --config=/etc/aide/aide.conf --check
Summary:
  Total number of entries:      47302
  Added entries:                2
  Removed entries:              1
  Changed entries:              9

The added and changed entries are attributable to normal activity in the interval between database generation and this check: shell history, less and vim history files, PAM faillock state, and log files that had simply grown (sudo.log, wtmp.db, AIDE’s own aideinit.log). None represent anomalies.

The recurring appearance of active log files in that list raises a broader question: does AIDE’s default configuration distinguish an actively written log file from a static, rotated one, or is every file under /var/log compared byte-for-byte on each run? The LOG HANDLING section of aide.conf defines the relevant groups:

ActLog = Full+growing+ANF+I
RotLog = Full
CompSerLog = Full+I+compressed
MidlSerLog = Full+I
LastSerLog = Full+ARF

ActLog is the group applied to a log’s currently active file. It includes the growing modifier, which tolerates size and content changes consistent with appended writes rather than flagging every append as a change. RotLog and the SerLog variants apply to already-rotated files, which are expected to stay static and are compared with a full hash.

Many well-known log paths are already matched against these groups by package-shipped fragments. The rsyslog fragment is a representative example:

# cat /etc/aide/aide.conf.d/31_aide_rsyslog
@@if not defined RSYSLOG_LOGDIR
@@define RSYSLOG_LOGDIR var/log
@@endif

/@@{RSYSLOG_LOGDIR}$ d VarDir

@@if not defined RSYSLOG_LOGFILE4RRE
@@define RSYSLOG_LOGFILE4RRE (syslog|messages|debug|(cron|lpr|auth|daemon|kern|user)\\.log|mail\\.(log|err|warn|info))
@@endif
@@if defined RSYSLOG_LOGFILE4RRE
 /@@{RSYSLOG_LOGDIR}/@@{RSYSLOG_LOGFILE4RRE}$ f ActLog
 /@@{RSYSLOG_LOGDIR}/@@{RSYSLOG_LOGFILE4RRE}\\.1$ f RotLog
 /@@{RSYSLOG_LOGDIR}/@@{RSYSLOG_LOGFILE4RRE}\\.2\\.@@{LOGEXT}$ f CompSerLog
 /@@{RSYSLOG_LOGDIR}/@@{RSYSLOG_LOGFILE4RRE}\\.3\\.@@{LOGEXT}$ f MidlSerLog
 /@@{RSYSLOG_LOGDIR}/@@{RSYSLOG_LOGFILE4RRE}\\.4\\.@@{LOGEXT}$ f LastSerLog
@@undef RSYSLOG_LOGFILE4RRE
@@endif

This fragment matches syslog, messages, debug, the cron/lpr/auth/daemon/kern/user log files, and the mail.* variants, applying the ActLog/RotLog/CompSerLog/MidlSerLog/LastSerLog groups to each. warnings.log, created locally in 6.1.2.5, matches none of these patterns and has no fragment of its own, so it does not receive the same growing-aware treatment. The @@{LOGEXT} variable referenced above resolves to gz, defined once in 10_aide_logext.

To confirm this concretely, the database is rebased and rechecked:

# aide --config=/etc/aide/aide.conf --update
# mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# aide --config=/etc/aide/aide.conf --check
Changed entries:
---------------------------------------------------

f   .ug    . .. .  : /var/lib/aide/aide.db
f >.... mc..H.. .  : /var/log/warnings.log

warnings.log is flagged as changed from ordinary log growth, confirming the gap. A matching fragment is written to a new file, 60_aide_warnings, mirroring the rsyslog fragment’s structure:

# cat /etc/aide/aide.conf.d/60_aide_warnings
/var/log/warnings\\.log$                    f ActLog
/var/log/warnings\\.log\\.1$                f RotLog
/var/log/warnings\\.log\\.2\\.@@{LOGEXT}$   f CompSerLog
/var/log/warnings\\.log\\.3\\.@@{LOGEXT}$   f MidlSerLog
/var/log/warnings\\.log\\.4\\.@@{LOGEXT}$   f LastSerLog

These are restricted rules, limited to file paths and groups. The full rule syntax is documented in aide.conf(5). The new fragment’s syntax is validated before use:

# aide --config=/etc/aide/aide.conf --config-check
# echo "$?"
0

According to aide(1), an exit status of 0 confirms no syntax errors. For --check, --compare, and --update, AIDE’s exit codes are additive bit flags representing what was found (new, removed, or changed entries), with a separate range of codes reserved for generic error conditions such as configuration or I/O failures.

The database is rebased again and rechecked:

# aide --config=/etc/aide/aide.conf --update
# mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# aide --config=/etc/aide/aide.conf --check
Summary:
  Total number of entries:      47306
  Added entries:                0
  Removed entries:              0
  Changed entries:              2

The two changed entries are the database file’s own ownership, reset from AIDE’s system user back to root as a side effect of manually promoting aide.db.new while logged in as root, and a systemd timer stamp file unrelated to logging. warnings.log no longer appears.

To confirm the growing tolerance directly rather than by absence, a new warning entry is generated and the check is run again:

# logger -p daemon.warning "after rule change test"
# aide --config=/etc/aide/aide.conf --check
Summary:
  Total number of entries:      47308
  Added entries:                1
  Removed entries:              0
  Changed entries:              2

The single added entry is AIDE’s own aide.log from the previous run, not warnings.log. The two changed entries are the same database ownership artifact and timer stamp as before. warnings.log is written to and does not register as changed, confirming the ActLog group’s growing tolerance applies correctly.

The benchmark also notes that prelink, if present, should be disabled before relying on AIDE, since prelinking rewrites binaries in ways that would otherwise trigger constant false positives.

# which prelink
# dpkg -l prelink
dpkg-query: no packages found matching prelink

prelink is not installed, consistent with the result in 1.5.6, so the note does not apply. Passes.

6.3.2 Ensure Filesystem Integrity Is Regularly Checked #

# systemctl list-unit-files | awk '$1~/^dailyaidecheck\.(timer|service)$/{print $1 "\t" $2}'
dailyaidecheck.service  static
dailyaidecheck.timer    enabled
# systemctl is-active dailyaidecheck.timer
active

Passes. The dailyaidecheck.timer unit is enabled and active, with its corresponding dailyaidecheck.service present as a static, timer-triggered unit.

6.3.3 Ensure Cryptographic Mechanisms Are Used to Protect the Integrity of Audit Tools #

Level 2, deferred.

A Lynis scan is run at the end of this section:

================================================================================

  Lynis security scan details:

  Scan mode:
  Normal [▆]  Forensics [ ]  Integration [ ]  Pentest [ ]

  Lynis modules:
  - Compliance status      [?]
  - Security audit         [V]
  - Vulnerability scan     [V]

  Details:
  Hardening index : 74 [##############      ]
  Tests performed : 278
  Plugins enabled : 2

  Software components:
  - Firewall               [V]
  - Intrusion software     [X]
  - Malware scanner        [X]

  Files:
  - Test and debug information      : /var/log/lynis.log
  - Report data                     : /var/log/lynis-report.dat

================================================================================

The hardening index moves to 74, a two-point increase attributable to AIDE alone. This stands out against the previous part, where journald configuration, rsyslog installation, log file permission corrections, and logrotate adjustments across all of 6.1 left the score exactly where it was. Lynis’s weighting evidently does not track the benchmark’s own section boundaries; a recommendation with comparatively little configuration effort, like enabling AIDE with its default rule set, can move the score more than an entire section of Level 1 hardening work.