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.