Untitled
unknown
sh
a month ago
22 kB
11
No Index
Never
#!/bin/bash #[redacted] # a library was loaded here that likely covers any mysterious gaps you find below (( ${EUID:-$(id -u)} == 0 )) || { printf -- '%s\n' "This must be run as root or with sudo" >&2 exit 1 } # Ensure that we're on RHEL 7 #if ! grep -q "Red Hat.*release 7" /etc/redhat-release; then # Cent added in for testing: #if ! grep -Eq "Red Hat.*release 7|Cent.*7" /etc/redhat-release; then # printf -- '%s\n' "This check is specific to RHEL-7" >&2 # exit 1 #fi # Undocumented feature: Allow debugging if we need it [[ "$*" = "*-x*" ]] && set -x ############################################################################### # Variables # Figure out the lowest boundary for the available UID range uid_min=$(awk '/^UID_MIN/{print $2}' /etc/login.defs) # Older releases of various Linux distros tended to use '500' as the minimum # So if we can't find it in login.defs, we'll default to '500' uid_min="${uid_min:-500}" HOSTNAME="${HOSTNAME:-$(hostname -s)}" conf_dir=/opt/redacted/etc/ ############################################################################### # Functions # A functionalised version of "if command -v prog >/dev/null 2>&1; then..." # Returns '0' if true, and '1' if not exists-cmd() { [[ $(command -v "${1:?null parameter}") ]] } # Function to check that a filesystem exists. # Returns '0' if true, and '1' if not exists-fs() { mount | grep -q "on ${1:?fs unset} " #grep -q " ${1:?fs unset} " /proc/mounts #alternative } exists-fs-logvol() { # First we need to establish that the fs exists if get-fs-device "${1:?fs unset}" >/dev/null 2>&1; then # Next, we test whether it has its own vg/lv lvs "$(get-fs-device "${1}")" >/dev/null 2>&1 # If the fs doesn't exist, we return 1 else return 1 fi } exists-iptables-rule() { local iptbl_policy="${1:?No iptables policy set}" local iptbl_protocol="${2:?No iptables protocol set}" local iptbl_port="${3:?No iptables port set}" local iptbl_action="${4:?No iptables action set}" iptables-save \ | grep -q -- "${iptbl_policy}.*-p ${iptbl_protocol}.*--dport ${iptbl_port}.*${iptbl_action}" } exists-swap() { free | grep -q "^Swap:.*[1-9]" } # Test if a variable is set in a more idiomatic way than [[ -n/-z ]] exists-var() { [[ "${1}" ]] } get-fs-device() { if exists-fs "${1:?fs unset}"; then df -hP "${1}" | tail -n 1 | awk '{print $1}' else return 1 fi } # This function will print out the size of the given filesystem get-fs-size() { df --output=size -m "${1:?fs unset}" | tail -n 1 | awk '{$1=$1};1' } # This function will print out the given filesystem type get-fs-type() { df -PT "${1:?fs unset}" | tail -n 1 | awk '{print $2}' } # List home directories for all local users get-home-dirs() { awk -F ':' -v min="${uid_min}" '{ if ( $3 >= min ) print $6 }' /etc/passwd } # List active network interface names get-network-interfaces() { ip a | awk -F':' '/^[0-9]/{print $2}' | tr -d " " | grep -v "^lo$" } # Commentary for this function is available in [redacted] get-memory-size() { local mem_total mem_total=$(awk '/MemTotal:/{ s+=$2 } END { printf("%0.f\n", s/1024) }' /proc/meminfo) (( ${#mem_total} == 0 )) && mem_total=$(free -m | awk '/Mem:/{print $2}') printf -- '%s\n' "${mem_total:-null}" } get-passwd-state() { case $(awk -F ':' -v user="${1}" '{if ($1 == user) print $2}' /etc/shadow) in (\$[1256]*) printf '%s\n' "P" # Password found ;; (\!|\*|\!\!|\!\*|\*LK\*) printf '%s\n' "LK" # Password or account locked ;; (NP|"") printf '%s\n' "NP" # No password set ;; esac } # Return 0 if a package is installed, 1 if it isn't get-package-state() { rpmquery --quiet "${1:?No pkg supplied}" && return "$?" } # Check if a service is enabled get-service-enabled() { systemctl list-unit-files | grep -q "${1:?svc unset}.*enabled" return "$?" } # Check if a service is active get-service-active() { systemctl | grep -q "${1:?svc unset}.service.*running" return "$?" } get-ssh-keys() { while read -r home_dir; do for file in "${home_dir}"/.ssh/*; do ssh-keygen -lf "${file}" 2>/dev/null done done < <(get-home-dirs) } get-swap-size() { local swap_total swap_total=$(awk '/SwapTotal:/{ s+=$2 } END { printf("%0.f\n", s/1024) }' /proc/meminfo) (( ${#swap_total} == 0 )) && swap_total=$(free -m | awk '/Swap:/{print $2}') printf -- '%s\n' "${swap_total:-null}" } get-system-accounts() { awk -F ':' -v min="${uid_min}" '{ if ( $3 < min ) print $1 }' /etc/passwd } get-system-uids() { awk -F ':' -v min="${uid_min}" '{ if ( $3 < min ) print $3 }' /etc/passwd } get-user-uids() { awk -F ':' -v min="${uid_min}" '{ if ( $3 >= min ) print $3 }' /etc/passwd } get-virtual-status() { if grep -q hypervisor /proc/cpuinfo; then printf '%s\n' "virtual" elif grep -qE '^flags.*svm|^flags.*vmx' /proc/cpuinfo; then printf '%s\n' "physical" else printf '%s\n' "unknown" fi } is-azure() { # All Azure hosts should have waagent.log grep -q -m 1 Azure /var/log/waagent.log 2>/dev/null && return "$?" # This may be a suitable alternative: #dmidecode | grep "String 1: \[MS_VM_CERT" # Does not work reliably: #blkid | grep -qE 'BEK|KEK' && return "$?" # The below might is not reliable, and technically it identifies HyperV: #dmesg | grep -q "Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine" } # From https://serverfault.com/a/903599 # See also: # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html is-aws() { local doc_url doc_url="http://169.254.169.254/latest/dynamic/instance-identity/document" if grep -q "^ec2" /sys/hypervisor/uuid 2>/dev/null; then return 0 elif grep -q "^EC2" /sys/devices/virtual/dmi/id/product_uuid 2>/dev/null; then return 0 elif curl -s -m 5 "${doc_url}" | grep -q availabilityZone; then return 0 else return 1 fi } # This function tries to determine if a host started life as a core install # This one is going to be a bit tricky and will likely need further work is-coreinstall() { # The approach I'm taking here is as follows: # grep for package groups (lines starting with @ e.g. @Base, @Core etc) # Quietly exclude '@^minimal' and '@core'. # If there's anything left, grep returns 0. # This indicates something like '@Base', so we flip the return code grep "^@" /root/anaconda-ks.cfg | grep -Eiqv '^@\^minimal|^@Core' && return 1 # Otherwise, we return ok. return 0 } # Function to test if a filesystem is encrypted is-fs-encrypted() { blkid | grep -q "${1:?No fs supplied}.*crytpo" && return "$?" } task_success() { out_array+=( "$(printf '[-] %s\n' "${@}")" ) } task_fail() { fail_array+=( "$(printf '[x] %s\n' "${@}")" ) } # Test if a given filesystem meets a minimum desired size # Usage: test-fs-size [fs] [type] test-fs-size() { case "$#" in (2) if (( $(get-fs-size "${1}") >= ${2} )); then task_fail "${1} does not appear to be correctly sized" else task_success "${1} appears to be correctly sized" fi ;; (*) task_fail "Misuse of test-fs-size function detected" return 1 ;; esac } # Test if a given filesystem is using the desired fs type # Usage: test-fs-type [fs] [type] test-fs-type() { case "$#" in (2) if [[ $(get-fs-type "${1}") = "${2}" ]]; then task_success "${1} appears to be a filesystem of type: ${2}" else task_fail "${1} does not appear to be a filesystem of type: ${2}" fi ;; (*) task_fail "Misuse of test-fs-type function detected" return 1 ;; esac } test-swap-size() { # If swap is enabled, ensure it is correctly sized relative to system memory if (( $(get-memory-size) <= 12288 )); then # In this scenario, swap should be half the memory size (or more) swap_target=$(( $(get-memory-size) / 2 )) if (( $(get-swap-size) >= swap_target )); then task_success "Swap appears to be correctly sized" else task_fail "Swap does not appear to be correctly sized (minimum: $(get-memory-size)M)" fi else # Otherwise, swap should max out at 8G if (( $(get-swap-size) != 8096 )); then task_fail "Swap does not appear to be correctly sized (maximum: 8096M)" elif (( $(get-swap-size) == 8096 )); then task_success "Swap appears to be correctly sized" fi fi } main () { ############################################################################### # Disks and VolGroups, Swap and Encryption # /boot needs to be xfs test-fs-type /boot xfs # and bigger than 1GB test-fs-size /boot 1024 # If we're on AWS, we expect to be under a single / with no swap if is-aws; then task_success "${HOSTNAME} appears to be AWS based" if exists-swap; then task_success "Swap appears to be present on this system" # Ensure that it is correctly sized test-swap-size ###### TO DO : Figure out whether swap is on encrypted EBS/ephemeral # Something like the below may be of use #bdmUrl="http://169.254.169.254/2012-01-12/meta-data/block-device-mapping/" #for bd in $(curl -s ${bdmUrl}); do # curl -s "${bdmUrl}${bd}" | \ # perl -pe 'BEGIN { $d = shift } s/^(\/dev\/)?(sd)?(.*)$/\/dev\/xvd$3 is $d\n/' "${bd}" #done | sort # See also https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html else task_fail "Swap is either not present or enabled on this system" fi # If we're on Azure, we expect a single / and # swapfile(s) in ephemeral disk under /mnt elif is-azure; then task_success "${HOSTNAME} appears to be Azure based" if exists-swap; then task_success "Swap appears to be present on this system" # Ensure that it is correctly sized test-swap-size # This was tricky to get right. If there's anything left in the output of # swapon *after* excluding our expected output, then things are wrong if swapon --noheadings | grep -v "^/mnt" | grep -q .; then task_fail "Swap file or device found outside Azure ephemeral storage" else task_success "Swap is correctly placed on Azure ephemeral storage" fi else task_fail "Swap is either not present or enabled on this system" fi # Otherwise, we expect our own filesystem layout else task_success "${HOSTNAME} appears to be traditional/on-premise" for fs in / /home /tmp /var /var/tmp /var/crash /var/log /var/log/audit; do if exists-fs "${fs}"; then task_success "${fs} appears to be its own filesystem" if exists-fs-logvol "${fs}"; then task_success "${fs} appears to have its own Logical Volume" else task_fail "${fs} does not appear to have its own Logical Volume" fi # All of these filesystem targets should be xfs test-fs-type "${fs}" xfs # And we test the following disk sizes [[ "${fs}" = "/" ]] && test-fs-size / 4096 [[ "${fs}" = "/home" ]] && test-fs-size /home 4096 [[ "${fs}" = "/tmp" ]] && test-fs-size /tmp 2048 [[ "${fs}" = "/var" ]] && test-fs-size /var 8192 [[ "${fs}" = "/var/tmp" ]] && test-fs-size /var/tmp 2048 [[ "${fs}" = "/var/log" ]] && test-fs-size /var/log 4096 [[ "${fs}" = "/var/log/audit" ]] && test-fs-size /var/log/audit 4096 else task_fail "${fs} does not appear to be its own filesystem" fi done # Ensure /tmp is encrypted if exists-fs /tmp; then if is-fs-encrypted /tmp; then task_success "/tmp appears to be encrypted" else task_fail "/tmp does not appear to be encrypted" fi fi # /var/crash needs to be present and 1.1x the size of memory if exists-fs /var/crash; then crash_target=$(bc <<< "$(get-memory-size) * 1.1") crash_size=$(get-fs-size /var/crash) if (( crash_size != crash_target )); then task_fail "/var/crash does not appear to be correctly sized (${crash_size} != ${crash_target})" else task_success "/var/crash appears to be correctly sized" fi # If it doesn't exist, we send a failure message else task_fail "/var/crash does not appear to be its own filesystem" fi # Ensure that swap is present and enabled if exists-swap; then task_success "Swap appears to be present on this system" # Ensure that it is correctly sized test-swap-size # Ensure that swap is encrypted if is-fs-encrypted swap; then task_success "Swap appears to be encrypted" else task_fail "Swap does not appear to be encrypted" fi else task_fail "Swap is either not present or enabled on this system" fi fi ############################################################################### # Software # This test uses task_success for both outputs as it's informational if is-coreinstall; then task_success "This host appears to have started life as a Core install" else task_success "This host does not appear to have started life as a Core install" fi # Optional packages as defined by ${conf_dir}/check_soe_pkgopt.conf # We want these packages to be installed if [[ -r "${conf_dir}/check_soe_pkgopt.conf" ]]; then while read -r pkg; do # If get-package-state returns 1, add the package to a list if ! get-package-state "${pkg}"; then pkg_fail="${pkg},${pkg_fail}" fi done < "${conf_dir}/check_soe_pkgopt.conf" # Remove any errant trailing commas pkg_fail="${pkg_fail%,}" # If the pkg_fail variable has any content, then we fail if exists-var "${pkg_fail}"; then task_fail "Optional packages appear to be missing: ${pkg_fail}" else task_success "All optional packages appear to be present" fi else task_fail "Optional package listing not provided or not readable" fi # Clear these two vars, ready for the next round of testing unset pkg pkg_fail # Excluded packages as defined by ${conf_dir}/check_soe_pkgexcl.conf # We want these packages to *not* be installed, so we take an opposite approach if [[ -r "${conf_dir}/check_soe_pkgexcl.conf" ]]; then while read -r pkg; do # If get-package-state returns 0, add the package to a list if get-package-state "${pkg}"; then pkg_fail="${pkg},${pkg_fail}" fi done < "${conf_dir}/check_soe_pkgexcl.conf" # Remove any errant trailing commas pkg_fail="${pkg_fail%,}" # If the pkg_fail variable has any content, then we fail if exists-var "${pkg_fail}"; then task_fail "Unwanted packages appear to be installed: ${pkg_fail}" else task_success "All unwanted packages appear to be absent" fi else task_fail "Package exclusion listing not provided or not readable" fi ############################################################################### # Firewall # Start by checking if we're running iptables if get-service-enabled iptables; then task_success "This host appears to be configured for 'iptables'" # Check that the service is active if get-service-active iptables; then task_success "'iptables' appears to be running" else task_fail "'iptables' does not appear to be running" fi # Now check our default policies if iptables -L -n | grep -q "INPUT (policy DROP)"; then task_success "'iptables' INPUT policy appears to be correct" else task_fail "'iptables' INPUT policy appears to be incorrect (expected: policy DROP)" fi if iptables -L -n | grep -q "FORWARD (policy DROP)"; then task_success "'iptables' FORWARD policy appears to be correct" else task_fail "'iptables' FORWARD policy appears to be incorrect (expected: policy DROP)" fi if iptables -L -n | grep -q "OUTPUT (policy ACCEPT)"; then task_success "'iptables' OUTPUT policy appears to be correct" else task_fail "'iptables' OUTPUT policy appears to be incorrect (expected: policy ACCEPT)" fi # Now test for the default rules # ssh, nrpe and check_mk: for port in 22 5556 6556; do if exists-iptables-rule INPUT tcp "${port}" ACCEPT; then task_success "Default rule for port ${port} exists" else task_fail "Default rule for port ${port} does not exist" fi done # snmp: if exists-iptables-rule INPUT udp 161 ACCEPT; then task_success "Default rule for port 161 exists" else task_fail "Default rule for port 161 does not exist" fi # Is firewalld enabled and running elif get-service-enabled firewalld; then task_success "This host appears to be configured for 'firewalld'" # Check that the service is active if get-service-active firewalld; then task_success "'firewalld' appears to be running" else task_fail "'firewalld' does not appear to be running" fi # Supposedly the same check method from iptables can be used # If not, I'll have to re-invent this... # ssh, nrpe and check_mk: for port in 22 5556 6556; do if exists-iptables-rule INPUT tcp "${port}" ACCEPT; then task_success "Default rule for port ${port} exists" else task_fail "Default rule for port ${port} does not exist" fi done # snmp: if exists-iptables-rule INPUT udp 161 ACCEPT; then task_success "Default rule for port 161 exists" else task_fail "Default rule for port 161 does not exist" fi # No firewall found else task_fail "This host does not appear to have a firewall configured" fi ############################################################################### # Networking case $(get-virtual-status) in (virtual) #if host is a vm, network interfaces should use ethx standard task_success "Host appears to be a virtual machine" if get-network-interfaces | grep -q eth; then task_success "Host appears to be correctly using ethx network naming standard" else task_fail "Host does not appear to be using ethx network naming standard" fi ;; (physical) #if host is physical, network interfaces should use "consistent naming" standard task_success "Host appears to be a physical machine" if get-network-interfaces | grep -q eth; then task_fail "Host does not appear to be using consistent naming standard" else # Careful wording - we don't assume that consistent naming is being used # All we have actually proved here is that ethx is not being used... task_success "Host does not appear to be using ethx network naming standard" fi ;; (unknown|*) task_fail "Could not determine if host is virtual or physical" ;; esac ############################################################################### # Security # Ensure SELinux is set to Enforcing mode if [[ "$(getenforce)" = "Enforcing" ]]; then task_success "SELinux appears to be in Enforcing mode" else task_fail "SELinux appears to not be in Enforcing mode" fi # Ensure TCP Wrappers is installed and has an ssh allow rule if rpm -qa | grep -q tcp_wrappers; then task_success "TCP Wrappers appears to be installed" if grep -qi "ssh.*all" /etc/hosts.allow; then task_success "TCP Wrappers ssh rule appears to exist" else task_fail "TCP Wrappers ssh rule does not appear to exist" fi else task_fail "TCP Wrappers does not appear to be installed" fi ############################################################################### # SSH # Check for certain key types if get-ssh-keys | grep -q ECDSA; then task_fail "ECDSA ssh keys were found on this host" else task_success "ECDSA ssh keys were not found on this host" fi if get-ssh-keys | grep RSA | grep -qv ^4096; then task_fail "RSA ssh keys with less than 4096 bits were found on this host" else task_success "RSA ssh keys on this host appear to be 4096 bits" fi ############################################################################### # Accounts # No system accounts with passwords for acct in $(get-system-accounts | grep -v root); do if [[ "$(get-passwd-state "${acct}")" == "P" ]]; then sys_acct_pwd=found task_fail "A system account was found with a configured password: ${acct}" fi done [[ ! "${sys_acct_pwd}" = "found" ]] && task_success "No system accounts with configured passwords" # No duplicate UID's if [[ $(get-system-uids | uniq -D &>/dev/null) ]]; then task_fail "Duplicate system UID's were found" else task_success "Duplicate system UID's were not found" fi if [[ $(get-user-uids | uniq -D &>/dev/null) ]]; then task_fail "Duplicate user UID's were found" else task_success "Duplicate user UID's were not found" fi # No duplicate home directories in /etc/passwd if [[ $(get-home-dirs | uniq -D &>/dev/null) ]]; then task_fail "Duplicate home directories exist on this host" else task_success "Duplicate home directories were not found on this host" fi } # Call the main function main if (( ${#fail_array[@]} > 0 )); then printf -- '%s\n' "Issues=${#fail_array[@]} This host is not compliant with the [redacted] SOE" \ "${fail_array[@]}" \ "${out_array[@]}" else printf -- '%s\n' "Issues=0 This host appears to be compliant with the [redacted] SOE" \ "${out_array[@]}" fi