Skip to content

gnathoi/macos-cis-hardening

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

macOS 26.x CIS Benchmark Hardening

A walkthrough record of remediating 26 failed CIS Benchmark checks on a Mac mini (Apple Silicon, M2) running macOS 26.x.

Context

A CIS Benchmark scan (cis_macOS_26.x.yml) reported 26 failed checks. This document records what each check actually meant, which were genuine issues, which were already in compliance, and the commands used to remediate.

⚠️ A Note on Scan Accuracy

Several "failed" checks were already in compliance on this machine. Verified before remediation:

Check Scan reported Actual state
#2 Firewall failed Already enabled (State = 1)
#3 Firewall Stealth Mode failed Already on
#4 FileVault failed Already On (fdesetup enable returned "already On")

These appear to be false negatives — most likely because the scan checks for a managed configuration profile (MDM-deployed) rather than the actual system state. On a non-managed personal machine, the underlying setting can be correctly configured but the scan flags it because no profile is present.

Lesson: Always verify the actual system state before assuming a "failed" check needs action.

Quick Reference

Status legend:

  • ✅ Applied & verified
  • ☑️ Already in compliance (false positive)
  • ⚠️ Command provided, verification pending
  • ⏭️ Skipped (not applicable or benign)
# Check Status
1 Install Application Updates from App Store ⚠️
2 Firewall enabled ☑️
3 Firewall stealth mode ☑️
4 FileVault enabled ☑️
5 Disable Help Apple Improve Search ⚠️
6 Disable Power Nap (Intel only) ⏭️ N/A on Apple Silicon
7 Custom login screen message
8 Login window: Name and Password ⚠️
9 Disable password hints ⚠️
10 On-Device Dictation ⏭️ Dictation never enabled (benign)
11 Security auditing (auditd) ⏭️ Subsystem deprecated in macOS 26
12 install.log retention 365+ days
13 Disable Bonjour advertising ✅ (effective on reboot)
14 Disable NFS server ⚠️
15 Password lockout threshold
16 Password minimum length (15)
17 Password must contain alphabetic
18 Password must contain numeric
19 Password must contain special
20 Password must contain mixed case
21 Password max age (365 days)
22 Password history (24)
23 Sudo timeout = 0
24 Root account disabled Partial — disabled, but shell fix ⚠️
25 Login window banner
26 Sudo logging enabled

Detailed Remediation Log

1. Install Application Updates from App Store

Auto-install of App Store app updates. Patches need to land without manual intervention to reduce vulnerability window.

sudo defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool TRUE

Requires logout/login for GUI to reflect.

2. Firewall — Already Enabled ☑️

Verified with:

/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
# Firewall is enabled. (State = 1)

No action needed. Scan false positive.

3. Firewall Stealth Mode — Already Enabled ☑️

Verified with:

/usr/libexec/ApplicationFirewall/socketfilterfw --getstealthmode
# Firewall stealth mode is on

No action needed. Scan false positive.

4. FileVault — Already Enabled ☑️

Verified with:

sudo fdesetup status
# FileVault is On.

No action needed. Scan false positive.

5. Disable "Help Apple Improve Search"

Privacy control. Stops Spotlight/Siri search queries from being sent to Apple.

defaults write com.apple.assistant.support "Search Queries Data Sharing Status" -int 2

Or GUI: System Settings → Spotlight → toggle off Help Apple Improve Search.

6. Power Nap — N/A on Apple Silicon ⏭️

Power Nap is an Intel-only feature. This M2 Mac mini doesn't have it. False positive — ignore.

7. Custom Login Screen Message ✅

sudo defaults write /Library/Preferences/com.apple.loginwindow LoginwindowText "If found, please contact nat@re.je"

To remove later: sudo defaults delete /Library/Preferences/com.apple.loginwindow LoginwindowText

8. Login Window: Name and Password

Hide the user list at login; require typing both username and password.

sudo defaults write /Library/Preferences/com.apple.loginwindow SHOWFULLNAME -bool true

Username on this machine is nat (short name) or Nathaniel Hey (full name) — either authenticates.

9. Disable Password Hints

sudo defaults write /Library/Preferences/com.apple.loginwindow RetriesUntilHint -int 0

Setting 0 means hint is never displayed regardless of failed attempts.

10. On-Device Dictation — Skipped ⏭️

Dictation was never enabled on this machine (preference key didn't exist). No voice data is leaving the device, so the underlying risk is moot. CIS check fails on a technicality but no action required.

11. Security Auditing (auditd) — Skipped ⏭️

Apple has effectively retired the BSM audit subsystem in macOS 26. The /var/audit/ directory doesn't exist; auditd loads but has nowhere to write. Security event logging continues via the unified log instead:

log show --predicate 'eventMessage contains "authentication"' --last 5m

CIS benchmark hasn't kept pace with macOS evolution. Accept as benchmark-vs-reality mismatch.

12. install.log Retention ✅

Modified /etc/asl/com.apple.install to retain logs ≥365 days with no size cap.

# Backup first
sudo cp /etc/asl/com.apple.install /etc/asl/com.apple.install.bak

# Apply new config
sudo sed -i '' 's|.*file /var/log/install.log.*|* file /var/log/install.log format='"'"'$((Time)(JZ)) $Host $(Sender)[$(PID)]: $Message'"'"' rotate=utc compress ttl=365|' /etc/asl/com.apple.install

Resulting line:

* file /var/log/install.log format='$((Time)(JZ)) $Host $(Sender)[$(PID)]: $Message' rotate=utc compress ttl=365

Changes: added ttl=365, removed file_max=50M and all_max=150M, changed rotate=seq to rotate=utc.

13. Disable Bonjour Advertising ✅

Stops the Mac from broadcasting itself + service info to the local network. Tradeoff: breaks AirDrop visibility, AirPlay receive, and similar discovery-based features.

sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements -bool true

launchctl kickstart is blocked by SIP — change takes effect on next reboot.

To revert:

sudo defaults delete /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements

14. Disable NFS Server

sudo nfsd disable

# Optional cleanup
sudo rm -f /etc/exports

15–22. Password Policy (Combined) ✅

Because pwpolicy setaccountpolicies is a replace operation (not merge), all eight password-related controls were applied as a single combined policy.

Settings applied (all CIS defaults):

# Control Value
15 Lockout threshold 5 attempts, 900s (15 min) lockout
16 Minimum length 15 characters
17 Must contain alphabetic Required
18 Must contain numeric Required
19 Must contain special Required
20 Must contain mixed case Required
21 Maximum age 365 days
22 History Cannot reuse last 24

Backup current policy first:

sudo pwpolicy -getaccountpolicies > ~/pwpolicy_backup_$(date +%Y%m%d).plist

Apply policy:

cat <<'EOF' | sudo pwpolicy setaccountpolicies /dev/stdin
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>policyCategoryAuthentication</key>
  <array>
    <dict>
      <key>policyContent</key>
      <string>(policyAttributeFailedAuthentications &lt; policyAttributeMaximumFailedAuthentications) OR (policyAttributeCurrentTime &gt; (policyAttributeLastFailedAuthenticationTime + autoEnableInSeconds))</string>
      <key>policyIdentifier</key>
      <string>Authentication Lockout</string>
      <key>policyParameters</key>
      <dict>
        <key>autoEnableInSeconds</key>
        <integer>900</integer>
        <key>policyAttributeMaximumFailedAuthentications</key>
        <integer>5</integer>
      </dict>
    </dict>
  </array>
  <key>policyCategoryPasswordContent</key>
  <array>
    <dict>
      <key>policyContent</key>
      <string>policyAttributePassword matches '.{15,}+'</string>
      <key>policyIdentifier</key>
      <string>Password Minimum Length 15</string>
    </dict>
    <dict>
      <key>policyContent</key>
      <string>policyAttributePassword matches '(.*[a-zA-Z].*)+'</string>
      <key>policyIdentifier</key>
      <string>Must Contain Alphabetic</string>
    </dict>
    <dict>
      <key>policyContent</key>
      <string>policyAttributePassword matches '(.*[0-9].*)+'</string>
      <key>policyIdentifier</key>
      <string>Must Contain Numeric</string>
    </dict>
    <dict>
      <key>policyContent</key>
      <string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*)+'</string>
      <key>policyIdentifier</key>
      <string>Must Contain Special</string>
    </dict>
    <dict>
      <key>policyContent</key>
      <string>policyAttributePassword matches '(.*[A-Z].*)+' AND policyAttributePassword matches '(.*[a-z].*)+'</string>
      <key>policyIdentifier</key>
      <string>Must Contain Mixed Case</string>
    </dict>
  </array>
  <key>policyCategoryPasswordChange</key>
  <array>
    <dict>
      <key>policyContent</key>
      <string>policyAttributeCurrentTime &lt; (policyAttributeLastPasswordChangeTime + (policyAttributeExpiresEveryNDays * 24 * 60 * 60))</string>
      <key>policyIdentifier</key>
      <string>Password Max Age 365</string>
      <key>policyParameters</key>
      <dict>
        <key>policyAttributeExpiresEveryNDays</key>
        <integer>365</integer>
      </dict>
    </dict>
    <dict>
      <key>policyContent</key>
      <string>none policyAttributePasswordHashes in policyAttributePasswordHistory</string>
      <key>policyIdentifier</key>
      <string>Password History 24</string>
      <key>policyParameters</key>
      <dict>
        <key>policyAttributePasswordHistoryDepth</key>
        <integer>24</integer>
      </dict>
    </dict>
  </array>
</dict>
</plist>
EOF

Verify:

sudo pwpolicy -getaccountpolicies

Notes:

  • Existing passwords are not affected — rules apply on next change.
  • 365-day max age means macOS will prompt for change in ~1 year.

Rollback:

sudo pwpolicy -clearaccountpolicies
# Or restore backup:
sudo pwpolicy setaccountpolicies ~/pwpolicy_backup_*.plist

23. Sudo Timeout = 0 ✅

Every sudo command now requires a fresh password — no 5-minute caching window.

echo "Defaults timestamp_timeout=0" | sudo tee /etc/sudoers.d/10_cissudoconfiguration
sudo chmod 440 /etc/sudoers.d/10_cissudoconfiguration
sudo chown root:wheel /etc/sudoers.d/10_cissudoconfiguration

Filename note: macOS ignores files in /etc/sudoers.d/ containing . — do not add an extension.

To revert: sudo rm /etc/sudoers.d/10_cissudoconfiguration

24. Root Account

Root was already disabled (AuthenticationAuthority key absent) but shell was /bin/sh. Set to /usr/bin/false so even non-interactive invocations of root produce no shell:

sudo dscl . -create /Users/root UserShell /usr/bin/false

Verify:

dscl . -read /Users/root UserShell
# UserShell: /usr/bin/false

25. Login Window Banner ✅

Pre-login text banner displayed before authentication. Distinct from the lock screen message in #7.

sudo tee /Library/Security/PolicyBanner.txt > /dev/null <<'EOF'
This system is for the use of authorized users only. Use of this system constitutes consent to monitoring. Unauthorized access is prohibited.
EOF
sudo chmod 644 /Library/Security/PolicyBanner.txt

To remove: sudo rm /Library/Security/PolicyBanner.txt

26. Sudo Logging ✅

Appends Defaults log_allowed to the sudoers config from #23. Privileged commands now logged to the unified log.

echo "Defaults log_allowed" | sudo tee -a /etc/sudoers.d/10_cissudoconfiguration

Final contents of /etc/sudoers.d/10_cissudoconfiguration:

Defaults timestamp_timeout=0
Defaults log_allowed

View sudo log entries:

log show --predicate 'process == "sudo"' --last 10m

Verification

After reboot, re-run the CIS scan. Expected remaining failures:

  • #6 (Power Nap) — N/A on Apple Silicon
  • #10 (Dictation) — only flags if dictation is enabled in on-device mode; safe to ignore if dictation is off
  • #11 (Security auditing) — deprecated subsystem

Items #1, #5, #8, #9, #14, #24 — verify these were applied since they were not directly confirmed during the walkthrough:

# #1
defaults read /Library/Preferences/com.apple.commerce AutoUpdate

# #5
defaults read com.apple.assistant.support "Search Queries Data Sharing Status"

# #8
defaults read /Library/Preferences/com.apple.loginwindow SHOWFULLNAME

# #9
defaults read /Library/Preferences/com.apple.loginwindow RetriesUntilHint

# #14
sudo nfsd status

# #24
dscl . -read /Users/root UserShell

Files Created/Modified

Path Purpose
/etc/asl/com.apple.install install.log retention config (with .bak backup)
/etc/sudoers.d/10_cissudoconfiguration Sudo timeout + logging
/Library/Security/PolicyBanner.txt Login window banner
~/pwpolicy_backup_YYYYMMDD.plist Pre-change password policy backup

Rollback Quick Reference

# Password policy
sudo pwpolicy -clearaccountpolicies

# Sudo timeout + logging
sudo rm /etc/sudoers.d/10_cissudoconfiguration

# install.log retention
sudo cp /etc/asl/com.apple.install.bak /etc/asl/com.apple.install

# Bonjour advertising
sudo defaults delete /Library/Preferences/com.apple.mDNSResponder.plist NoMulticastAdvertisements

# Login banner
sudo rm /Library/Security/PolicyBanner.txt

# Login screen message
sudo defaults delete /Library/Preferences/com.apple.loginwindow LoginwindowText

# Login window: revert to user list
sudo defaults delete /Library/Preferences/com.apple.loginwindow SHOWFULLNAME

# Password hints
sudo defaults delete /Library/Preferences/com.apple.loginwindow RetriesUntilHint

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors