---
title: "macOS Hardening in Practice: Auditing 54 Controls, Detecting Drift, and Remediating Without Dependencies"
description: "A deep dive into macOS endpoint security architecture: auditing SIP, Gatekeeper, TCC, LaunchDaemons, BPF packet filters, and plaintext secret leakage using native Zsh primitives, zero third-party dependencies, and OASIS SARIF v2.1.0."
date: 2026-11-09
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/macharden-macos-security-audit-hardening/
---

## The myth of default macOS security

The claim that *"macOS is secure by default"* is dangerous because it mistakes platform security features for operational workstation posture.

Out of the box, Apple ships modern macOS with formidable protections: System Integrity Protection (SIP) locks down system directories and NVRAM variables, FileVault encrypts the APFS container with XTS-AES-128, and Gatekeeper enforces code-signing and developer notarization.

```bash
# What the defaults look like on a fresh macOS installation
$ csrutil status
System Integrity Protection status: enabled.

$ spctl --status
assessments enabled

$ fdesetup status
FileVault is On.
```

However, the moment a developer or security engineer configures that Mac for daily work, those default guarantees begin to unravel:

1. **Homebrew and Inbound Sockets:** Development tools (like local Python, Node.js, and Docker daemons) trigger firewall prompts. Over time, developers allow inbound connections, and `socketfilterfw` quietly whitelists interpreters across user directories.
2. **Packet Filter Permissions:** Installing Wireshark or capture utilities creates the `access_bpf` group and installs a LaunchDaemon changing `/dev/bpf*` permissions to `0660`. Non-root processes can now capture unencrypted loopback and local network traffic without administrative passwords.
3. **Secret Accumulation:** Shell profiles (`~/.zshrc`, `~/.bashrc`) accumulate high-value cloud tokens (`AWS_SECRET_ACCESS_KEY`, `OPENAI_API_KEY`, GitHub personal access tokens). Development projects leave world-readable `.env` files with database credentials on disk.
4. **Persistence and Unsigned Helpers:** Third-party developer tools install background daemons into `/Library/LaunchDaemons/` and `/Library/PrivilegedHelperTools/`. If a vendor removes an uninstalled binary without cleaning its plist, a dangling LaunchDaemon is left behind, waiting for privilege escalation or binary hijacking.

To evaluate, track, and remediate this attack surface without introducing third-party risk, we engineered [`macharden`](https://github.com/jankesec/macharden).

## The 4 audit domains of macharden

`macharden` organizes 54 CIS and NIST-aligned security checks across four operational domains:

<figure class="diagram">
<svg viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="macharden 4-Domain macOS Security Model: Hardening, Network, Persistence, and Secrets audited via native system binaries.">
<defs><marker id="machar-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="16" class="dg-muted">MACOS SECURITY ARCHITECTURE · 4 AUDIT DOMAINS (54 CONTROLS)</text>
<rect x="0" y="40" width="175" height="120" rx="8" class="dg-box" />
<text x="14" y="66" class="dg-label">1 · OS Hardening</text>
<text x="14" y="88" class="dg-mono dg-compact">SIP / FileVault</text>
<text x="14" y="108" class="dg-muted">Gatekeeper & Boot</text>
<line x1="175" y1="100" x2="195" y2="100" class="dg-line" />
<rect x="195" y="40" width="175" height="120" rx="8" class="dg-box-accent" />
<text x="209" y="66" class="dg-label">2 · Network</text>
<text x="209" y="88" class="dg-mono dg-compact">ALF & BPF Filters</text>
<text x="209" y="108" class="dg-muted">Stealth & AWDL</text>
<line x1="370" y1="100" x2="390" y2="100" class="dg-line" />
<rect x="390" y="40" width="175" height="120" rx="8" class="dg-box" />
<text x="404" y="66" class="dg-label">3 · Persistence</text>
<text x="404" y="88" class="dg-mono dg-compact">Launchd & Sudo</text>
<text x="404" y="108" class="dg-muted">Privileged Helpers</text>
<line x1="565" y1="100" x2="585" y2="100" class="dg-line" />
<rect x="585" y="40" width="175" height="120" rx="8" class="dg-box-danger" />
<text x="599" y="66" class="dg-label">4 · Secrets</text>
<text x="599" y="88" class="dg-mono dg-compact">Env & SSH Keys</text>
<text x="599" y="108" class="dg-muted">Plaintext Profiles</text>
</svg>
<figcaption>macharden 4-domain macOS security architecture: 54 controls auditing operating system integrity, network ingress, execution persistence, and secret exposures.</figcaption>
</figure>

| Category | Weight Range | Focus Areas | Key Security Primitives |
| :--- | :--- | :--- | :--- |
| **Hardening** | 4 – 10 | System integrity, kernel flags, storage encryption, lock policies | `csrutil`, `fdesetup`, `spctl`, `defaults`, `bputil` |
| **Network** | 4 – 9 | Inbound firewall, stealth mode, packet filter permissions, wireless | `socketfilterfw`, `/dev/bpf*`, `ifconfig`, `awdl0` |
| **Persistence** | 5 – 8 | Startup items, background daemons, sudoers, code signatures | `launchctl`, `plutil`, `/Library/LaunchDaemons`, `codesign` |
| **Secrets** | 5 – 9 | Plaintext tokens in shell configs, exposed `.env`, Keychain locks | `~/.zshrc`, `~/.docker/config.json`, `kern.coredump`, SSH keys |

## Zero-dependency auditing: native primitives vs heavy agents

Most commercial endpoint security agents require installing 150MB+ runtimes (Electron, Python, or heavy background daemons) that run as root. This introduces new vulnerabilities, triggers Gatekeeper and TCC authorization prompts, and consumes substantial system resources.

`macharden` takes the opposite design path: **zero external dependencies**. It executes purely through native macOS Zsh and system binaries:

- File and plist parsing is handled directly with `plutil` and `defaults`.
- Binary signatures and notarization are checked using `/usr/bin/codesign -dv`.
- Sudoers rules, groups, and directory ACLs are evaluated with `dseditgroup`, `stat`, `awk`, and `sed`.

To prevent checks from hanging when an internal tool triggers an interactive authentication prompt or keychain dialog, `macharden` wraps queries in an isolated deadline runner using a native Perl fork:

```zsh
# macharden lib/audit_secrets.sh: Strict execution deadline wrapper
_sec_run_limited() {
    local secs="${1:-5}"
    shift
    [[ $# -gt 0 ]] || return 1

    perl -e '
        use strict;
        my $timeout = shift @ARGV;
        my $pid = fork();
        if (!defined $pid) { exit 1 }
        if ($pid == 0) {
            open(STDIN, "<", "/dev/null") or exit 1;
            open(STDERR, ">", "/dev/null");
            exec { $ARGV[0] } @ARGV;
            exit 127;
        }
        $SIG{ALRM} = sub {
            kill "TERM", $pid;
            select(undef, undef, undef, 0.2);
            kill "KILL", $pid;
            exit 142;
        };
        alarm $timeout;
        waitpid($pid, 0);
        exit ($? >> 8);
    ' "$secs" "$@"
}
```

Furthermore, when scanning shell configurations for leaked API keys, `macharden` uses strict regex extraction that logs **only the variable name** (`NAME=***`). Raw secret values are discarded in memory and never written to reports, stdout, or SARIF output.

## Real workstation audit walkthrough

Running an audit on an engineer's everyday Apple Silicon workstation:

```bash
# Run full 54-control audit across all domains
macharden audit --json report.json --sarif report.sarif --html report.html
```

### 1. Hardening Index and category posture breakdown

The engine computes a weighted Hardening Index between 0% and 100%, penalizing critical-weight controls more heavily than minor informational deviations:

```text
# macOS Security Hardening Audit Report
Hardening Index: 82.2% — GOOD / ACCEPTABLE (Grade: B+)

Total Checks Audited:   50
Passed Checks:          30  (🟢 PASS)
Warnings:               10  (🟡 WARN)
Failed Checks:           2  (🔴 FAIL)
Informational:           5  (🔵 INFO)
Suggestions:             3  (🟣 SUGG)
Earned Points:          235.0 / 286.0
```

The category posture breakdown reveals where the machine was actually vulnerable:

```text
Category Posture Breakdown:
Hardening     100.0%  [🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩]  🟢 PASS       (96.0 / 96.0 pts)
Network        82.0%  [🟨🟨🟨🟨🟨🟨🟨🟨⬜⬜]  🟡 WARN       (61.5 / 75.0 pts)
Persistence    85.9%  [🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜]  🟢 PASS       (39.5 / 46.0 pts)
Secrets        55.1%  [🟥🟥🟥🟥🟥⬜⬜⬜⬜⬜]  🔴 DEFICIENT  (38.0 / 69.0 pts)
```

The system scored 100% on core OS hardening (SIP, FileVault, and Gatekeeper were all properly enforced). The failures occurred in the **Network** and **Secrets** domains.

### 2. High-risk findings on a real developer machine

Inspecting the specific audit warnings and failures uncovered classic operational security debt:

#### Finding 1: Application Firewall allowed incoming connections to Python (`NET-03`)

```bash
$ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --listapps
# /opt/homebrew/Cellar/python@3.14/3.14.7/.../Python.app (Incoming: ALLOWED)
```
When debugging local development servers, the engineer approved an incoming connection prompt. The Application Firewall retained an indefinite inbound rule for an arbitrary Python interpreter, allowing any unauthenticated script binding to a port to receive remote network traffic.

#### Finding 2: Unprivileged BPF packet capture permitted (`NET-04`)

```bash
$ ls -la /dev/bpf*
crw-rw----  1 root  access_bpf  0x17, 0x00 Nov  9 02:00 /dev/bpf0
```
Wireshark had added the user to `access_bpf`. Any unprivileged script or compromised user process could open `/dev/bpf0` and sniff raw ethernet frames without `sudo`.

#### Finding 3: Plaintext API keys in shell profiles (`SEC-01`)

```text
🔴 FAIL [SEC-01] Plaintext secrets found in shell profiles (4):
  .zshrc:6   (H1_API_TOKEN=***)
  .zshrc:24  (DEEPSEEK_API_KEY=***)
  .zshrc:28  (HUNT_PX_TOKEN=***)
  .zshrc:56  (OPENAI_API_KEY=***)
```
API keys were exported directly in `.zshrc`. Any local script reading dotfiles could exfiltrate them immediately.

#### Finding 4: Insecure Docker credentials permissions (`SEC-11`)

```bash
$ ls -la ~/.docker/config.json
-rw-r--r--  1 johndoe  staff  342 Nov  3 12:00 ~/.docker/config.json
```
The Docker CLI configuration stored base64-encoded registry authentication tokens with `644` (world-readable) permissions. Any unprivileged local user could read private registry credentials.

### 3. Interactive Liquid Glass dashboard and SARIF export

In addition to terminal output, `macharden` produces a self-contained, single-file HTML5 dashboard utilizing a zero-dependency Liquid Glass aesthetic:

<figure class="evidence">
<a href="/images/posts/macharden-liquid-glass-dashboard-evidence.webp" target="_blank" rel="noopener" aria-label="Open the full macharden Liquid Glass dashboard screenshot"><img src="/images/posts/macharden-liquid-glass-dashboard-evidence.webp" alt="macharden Liquid Glass interactive HTML5 dashboard showing 82.2 percent Hardening Index, category scores, and remediation playbook" loading="lazy" /></a>
<figcaption><span>macharden Dashboard</span> Single-file, dependency-free HTML5 report displaying the weighted Hardening Index, category posture breakdown, and actionable remediation steps.</figcaption>
</figure>

The output can also be emitted directly in OASIS SARIF v2.1.0 format (`--sarif report.sarif`), enabling integration with GitHub Code Scanning:

```json
{
  "version": "2.1.0",
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "macharden",
          "version": "1.3.0",
          "informationUri": "https://github.com/jankesec/macharden"
        }
      },
      "results": [
        {
          "ruleId": "SEC-11",
          "level": "error",
          "message": { "text": "Sensitive credential files have insecure permissions: ~/.docker/config.json (644, expected 600)" }
        }
      ]
    }
  ]
}
```

## Baseline drift tracking: macharden diff

Security is not a one-time configuration; it is an ongoing state. When an engineer updates their environment, tests new software, or modifies sharing settings, security configurations inevitably drift.

`macharden` provides native drift tracking without requiring a central database:

```bash
# 1. Save initial hardened baseline
macharden audit --json baseline.json

# 2. Later, audit the current state and compute the drift
macharden diff baseline.json current.json
```

The diff engine analyzes changes between both snapshots, highlighting regressions, fixes, and score deltas:

```text
╭─ Baseline Drift Analysis ────────────────────────────────────────────────────╮
│ Baseline: 2026-09-12 18:58:06  (82.2% - Grade: B+)                           │
│ Current:  2026-11-09 02:45:10  (87.4% - Grade: A-)                           │
│ Delta:    +5.2% (IMPROVED)                                                   │
╰──────────────────────────────────────────────────────────────────────────────╯

RESOLVED DEFICIENCIES (2):
  🟢 [SEC-11] Cloud and API Credentials File Permissions: FAIL -> PASS
  🟢 [NET-03] Firewall Permissive Exceptions: WARN -> PASS

NEW REGRESSIONS (0):
  No posture regressions detected.
```

## Automated and dry-run remediation

An audit report without actionable fixes creates cognitive fatigue. `macharden` pairs every rule with an exact, deterministic remediation command.

Before applying any changes, administrators can verify what will happen using `--dry-run`:

```bash
# Review exact remediation actions without mutating system state
macharden remediate --dry-run
```

Sample remediation execution for the findings identified above:

```bash
# Remediate NET-03: Block dangerous inbound interpreter exceptions
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --blockapp \
  "/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/Resources/Python.app"

# Remediate NET-04: Remove non-root BPF packet capture permissions
sudo dseditgroup -o edit -d johndoe -t user access_bpf 2>/dev/null
sudo chmod 600 /dev/bpf*

# Remediate SEC-11: Restrict Docker credentials file permissions
chmod 600 ~/.docker/config.json
```

| Finding ID | Threat Mechanism | Attack Surface | Remediation Primitive |
| :--- | :--- | :--- | :--- |
| `NET-03` | Inbound socket access | Unauthenticated remote access to Python processes | `socketfilterfw --blockapp` |
| `NET-04` | Non-root BPF access | Unprivileged packet sniffing on local interfaces | `dseditgroup -d`, `chmod 600 /dev/bpf*` |
| `PERS-07` | Unsigned privileged helper | Privilege escalation via hijacked helper binary | `launchctl bootout`, remove unsigned plist |
| `SEC-11` | World-readable credentials | Local credential theft from `~/.docker/config.json` | `chmod 600 ~/.docker/config.json` |

## Evidence and limits

All testing and audit verification were conducted locally on macOS 15 (Sequoia) running on Apple Silicon hardware (M-series). System Integrity Protection (SIP), Gatekeeper, and FileVault audits reflect active kernel-level enforcement. While `macharden` identifies and remediates local OS-level misconfigurations, hardware-bound protections (such as Secure Enclave passcode policies and Recovery Mode MDM enrollment) require interactive recovery console access or Mobile Device Management profiles.

## References

- **jankesec, macharden (macOS Security Audit & Hardening Engine):** [github.com/jankesec/macharden](https://github.com/jankesec/macharden)
- **Center for Internet Security, CIS Apple macOS Benchmark:** [cisecurity.org/benchmark/apple_os](https://www.cisecurity.org/benchmark/apple_os)
- **NIST, Security and Privacy Controls for Information Systems and Organizations (SP 800-53 Rev. 5):** [csrc.nist.gov/pubs/sp/800/53/r5/upd1/final](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final)
- **MITRE ATT&CK, macOS Enterprise Matrix:** [attack.mitre.org/matrices/enterprise/macos](https://attack.mitre.org/matrices/enterprise/macos/)