SELinux & Linux Hardening - Street-Level Ops¶
What experienced ops engineers know about running hardened Linux systems without losing their minds.
Quick Diagnosis Commands¶
# SELinux status and mode
getenforce
sestatus
# Recent SELinux denials
ausearch -m avc -ts recent
ausearch -m avc -ts today --interpret
# Human-readable denial explanations
sealert -a /var/log/audit/audit.log | head -80
# Check SELinux context on files
ls -Z /var/www/html/
ls -Z /etc/nginx/
# Check SELinux context on processes
ps -eZ | grep -E 'httpd|nginx|sshd'
# Check SELinux booleans for a service
getsebool -a | grep httpd
getsebool -a | grep nginx
# Check listening ports and their SELinux labels
semanage port -l | grep http
semanage port -l | grep ssh
# Audit log summary
aureport --avc --summary
# Check for failed logins
lastb | head -20
faillock --user <username>
# Check sysctl values
sysctl -a | grep ip_forward
sysctl -a | grep syncookies
# Check for world-writable files
find /etc -type f -perm -0002 2>/dev/null
# Check SUID binaries
find /usr -type f -perm -4000 -exec ls -la {} \; 2>/dev/null
Gotcha: Application Breaks After Enabling SELinux¶
You enabled SELinux enforcing mode on a server that has been running permissive for months. Half the services stop working. The application cannot read its own files, bind to its port, or connect to the database.
Fix:
# Step 1: Switch to permissive first (log but don't block)
setenforce 0
# Step 2: Relabel the entire filesystem
fixfiles -F onboot
# Or for RHEL 9+:
touch /.autorelabel
reboot
# Step 3: After relabel, check for denials in permissive mode
ausearch -m avc -ts boot
# Step 4: Fix the most common issues
restorecon -Rv /var/www/
restorecon -Rv /opt/myapp/
semanage port -a -t http_port_t -p tcp 8080
setsebool -P httpd_can_network_connect on
# Step 5: Only switch to enforcing when denials are clean
setenforce 1
Never go from disabled to enforcing without a relabel. The filesystem has no labels and everything will be denied.
One-liner: The relabel-on-boot (
touch /.autorelabel) walks every file on every filesystem and sets the correct SELinux context based on the policy's file context rules. On a server with millions of files, this can take over an hour. Schedule it during a maintenance window, not during a production incident.
Gotcha: Custom App Files Have Wrong SELinux Context¶
You deployed your application to /opt/myapp/ and files have default_t context. SELinux blocks the web server from reading them. restorecon does not help because there is no policy for that path.
Fix:
# Define a custom file context rule
semanage fcontext -a -t httpd_sys_content_t "/opt/myapp/static(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t "/opt/myapp/uploads(/.*)?"
semanage fcontext -a -t httpd_log_t "/opt/myapp/logs(/.*)?"
# Apply the context rules
restorecon -Rv /opt/myapp/
# Verify
ls -Z /opt/myapp/
Gotcha: audit2allow Generates Overly Broad Policies¶
You run ausearch -m avc | audit2allow -M fix and install it. It works. But the generated policy allows far more than the specific action that was denied. You just punched a hole in your security policy.
Fix:
# Always review the generated policy first
ausearch -m avc -ts recent | audit2allow -m mypolicy > mypolicy.te
cat mypolicy.te
# Look for overly broad rules like:
# allow httpd_t file_type:file { read open getattr };
# This allows httpd to read ALL file types, not just web content
# Instead, write targeted rules manually
cat > myapp.te << 'EOF'
module myapp 1.0;
require {
type httpd_t;
type myapp_data_t;
class file { read open getattr };
}
allow httpd_t myapp_data_t:file { read open getattr };
EOF
checkmodule -M -m -o myapp.mod myapp.te
semodule_package -o myapp.pp -m myapp.mod
semodule -i myapp.pp
Gotcha: SSH Hardening Locks You Out¶
You pushed SSH hardening via Ansible: PasswordAuthentication no, AllowUsers deploy. But the deploy user's SSH key is not on the target server. You just locked yourself out of every server in the fleet.
Fix:
# Prevention: Always test SSH config before restarting
sshd -t # Syntax check
sshd -T # Show effective config
# In Ansible, validate before applying:
# - name: Validate sshd config
# command: sshd -t -f /etc/ssh/sshd_config.new
# register: sshd_check
# changed_when: false
#
# - name: Deploy sshd config
# copy:
# src: sshd_config
# dest: /etc/ssh/sshd_config
# when: sshd_check.rc == 0
# Recovery: If locked out
# 1. Console access (IPMI/iLO/iDRAC/cloud console)
# 2. Boot to single-user mode
# 3. Fix /etc/ssh/sshd_config
# 4. systemctl restart sshd
Always maintain out-of-band access (console, IPMI) on hardened servers. SSH is not your only door.
Gotcha: When using Ansible to harden SSH, always use a two-step deploy: first push the new config to a temporary file and validate with
sshd -t, then atomically move it into place. If you push a broken config directly to/etc/ssh/sshd_configand restart sshd, existing sessions may survive but no new connections will work — and you may not notice until your current session ends.
Gotcha: faillock Locks Out Legitimate Users¶
You configured pam_faillock with deny=3 unlock_time=900. An attacker brute-forces your admin account. The account locks. Your legitimate admin cannot log in for 15 minutes during an incident.
Fix:
# Unlock a specific user
faillock --user admin --reset
# Check lock status
faillock --user admin
# Better config: longer lockout but allow console bypass
# In /etc/pam.d/system-auth, add before faillock:
# auth [success=1 default=ignore] pam_succeed_if.so service = login
# This skips faillock for local console logins
Pattern: Hardened AMI Build Pipeline¶
# Packer + Ansible pipeline for hardened AMIs
# 1. Start from official vendor AMI
# 2. Run CIS hardening playbook
# 3. Run SELinux policy setup
# 4. Run sysctl hardening
# 5. Scan with OpenSCAP
# 6. Publish AMI
# OpenSCAP scan for CIS compliance
oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis \
--results scan-results.xml \
--report scan-report.html \
/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
# Parse results
oscap xccdf generate report scan-results.xml > report.html
Pattern: Ongoing Compliance Monitoring¶
#!/bin/bash
# compliance-check.sh - run via cron weekly
set -euo pipefail
REPORT_DIR=/var/log/compliance
mkdir -p "$REPORT_DIR"
DATE=$(date +%Y-%m-%d)
# Check SELinux is enforcing
MODE=$(getenforce)
[ "$MODE" != "Enforcing" ] && echo "FAIL: SELinux is $MODE" >> "$REPORT_DIR/$DATE.log"
# Check for world-writable files in /etc
WORLD_WRITABLE=$(find /etc -type f -perm -0002 2>/dev/null | wc -l)
[ "$WORLD_WRITABLE" -gt 0 ] && echo "FAIL: $WORLD_WRITABLE world-writable files in /etc" >> "$REPORT_DIR/$DATE.log"
# Check SSH config
grep -q "^PermitRootLogin no" /etc/ssh/sshd_config || \
echo "FAIL: root login not disabled" >> "$REPORT_DIR/$DATE.log"
grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config || \
echo "FAIL: password auth not disabled" >> "$REPORT_DIR/$DATE.log"
# Check sysctl values
[ "$(sysctl -n net.ipv4.ip_forward)" != "0" ] && \
echo "FAIL: IP forwarding enabled" >> "$REPORT_DIR/$DATE.log"
# Check for new SUID binaries since last scan
find /usr /bin /sbin -type f -perm -4000 2>/dev/null | sort > "$REPORT_DIR/suid-$DATE.list"
if [ -f "$REPORT_DIR/suid-baseline.list" ]; then
diff "$REPORT_DIR/suid-baseline.list" "$REPORT_DIR/suid-$DATE.list" || \
echo "WARN: SUID binaries changed" >> "$REPORT_DIR/$DATE.log"
fi
echo "Compliance scan complete: $REPORT_DIR/$DATE.log"
Emergency: SELinux Preventing Boot or Login¶
# 1. At GRUB menu, add to kernel command line:
# enforcing=0
# This boots in permissive mode without changing config
# 2. Once booted, check what's wrong
ausearch -m avc -ts boot
# 3. Fix the issue (usually a relabel is needed)
fixfiles -F onboot
reboot
# 4. If relabel fixes it, switch back to enforcing
setenforce 1
# Verify in /etc/selinux/config: SELINUX=enforcing
# Nuclear option (only if system is completely broken):
# Add selinux=0 to kernel command line
# This disables SELinux entirely — use only for recovery
# You MUST relabel before re-enabling
Emergency: Auditd Filling Disk¶
# 1. Check audit log size
du -sh /var/log/audit/
# 2. Rotate immediately
service auditd rotate
# 3. Check what's generating the most events
aureport --summary
# 4. Reduce noisy rules
auditctl -l | grep -c "" # Count current rules
# 5. Temporarily reduce logging
auditctl -e 0 # Disable auditing (TEMPORARY)
# 6. Fix rules, then re-enable
auditctl -e 1
# 7. Set proper log rotation in /etc/audit/auditd.conf
# max_log_file = 50
# num_logs = 5
# max_log_file_action = ROTATE