Skip to content

macOS setup with file/process allowlists, system integrity checks, mSCP profile, local DNS server, and SSH/GPG hardening

License

Notifications You must be signed in to change notification settings

crimsonpython24/macsetup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

macOS Setup

Elevator Pitch

Why this exists: Most people trust Apple's defaults, but those prioritize convenience over security: processes can persist on startup, DNS queries leak browsing history, system file changes go unaudited, and background services (like iCloud sync) run even when disabled in settings. This setup blocks unauthorized programs and allows manual tracing when processes modify critical directories.

What it achieves: The system blocks unauthorized executables, encrypts and validates all DNS traffic, maintains cryptographic checksums of system files, and complies with government security baselines (CNSSI-1253 in particular).

Component What It Does Key Features
Application allowlisting (Santa) Block unauthorized executables • Signing ID-based blocking
• Protect sensitive files (SSH keys, configs)
NIST compliance (mSCP) Apply government-level security standards • CNSSI-1253 baseline (~96-100% compliance)
• Enforce telemetry and firewall rules
File integrity monitoring (AIDE) Detect unauthorized file changes • Monitor LaunchDaemons/LaunchAgents
• Detect PAM modifications
DNS hardening Encrypt and validate DNS queries • DNSCrypt-proxy for encrypted queries
• Unbound for DNSSEC validation + caching
Application isolation Limit app privileges • Restrict write access to system directories
• Prevent persistence in /Library paths
Shell environment Secure development tools • Hardened SSH (modern algorithms, multiplexing)
• GPG for commit signing

Time investment: ~90 minutes from a fresh install with decent Internet connectivity.

This is not the most convenient, most resource-optimized, or most secure configuration. It is simply what works best for me.

0. Basics

  • launchd should not be modified like systemctl as the former is not designed for user tweaks
    • This rule also applies to "UI skins," custom plugins, etc. since they might break with future updates
    • I.e., one should not change any obscure settings (e.g., defaults write com.apple.something) unless they know exactly what they are doing
  • Do not use the admin account besides initializing the machine
  • Do not install any apps as Admin unless necessary, as some should run just fine outside the root directory (e.g. /Users/warren/Applications)
    • Said apps will prompt for password if they need privilege escalations regardless, and it is up to one to decide whether to escalate its privilege (install in /Applications) or find an alternative.
    • Do not move pre-/auto-installed macOS apps around in Finder, because future updates might break those modifications
  • Only make system-wide configurations (e.g., network interface) or run services such as ClamAV and Santa in Admin; for things like gpg keys, set up in individual users
  • Terminal
    • Ensure that Secure keyboard (in Terminal and iTerm) is enabled
    • Use MacPorts instead of Brew
    • Implement some hardware hardening:
# Cold boot attacks
sudo pmset -a destroyfvkeyonstandby 1 hibernatemode 25 standbydelaylow 0 standbydelayhigh 0 highstandbythreshold 0 powernap 0 tcpkeepalive 0 proximitywake 0

# Reduce unified log retention
sudo log config --mode "level:off" --subsystem com.apple.diagnosticd

# Prevents SUID programs from dumping core
sudo sysctl -w kern.sugid_coredump=0

# Secure virtual memory
sudo defaults write /Library/Preferences/com.apple.virtualMemory UseEncryptedSwap -bool YES
defaults read /Library/Preferences/com.apple.virtualMemory UseEncryptedSwap
# 1

Note This guide reflects the "secure, not private" concept in that, although these settings make the OS more secure than the shipped or reinstalled version, this does not guarantee privacy from first-party or three-letter-agencies surveillance.

https://taoofmac.com/space/howto/switch#best-practices
https://news.ycombinator.com/item?id=31864974
https://github.com/beerisgood/macOS_Hardening?tab=readme-ov-file

1. CLI Tools

Until section 7, this tutorial should be done in admin's GUI because some sections require running a privileged script, but admin cannot read/write warren's files if downloaded there, even if switched to warren in admin's terminal. I.e., the only way that works is to download and run sudo in admin's GUI. su - warren does not work with all commands in that user.

  • Install xcode CLI tools/MacPorts in only one account (admin) to prevent duplicate instances and PATH confusion (technically port can only be installed in admin because it needs sudo access, but still).
  • After xcode-select --install, install MacPorts. Quit and re-open Terminal.

2. Santa Setup

For the following sections, all dependencies can be installed via MacPorts. Avoid using third-party pkg/dmg installers to keep dependency tree clean.

Remember to create the non-privileged user first to ensure sections 2-4 also apply to it without having to double-check later.

  1. First create the Warren user (hello!). Log in, go through the setup, and make sure the Warren account works. Also go to Settings > "Menu Bar" > "Fast User Switching" and toggle it on. Switch back to admin's GUI.
  2. Add MacPorts to warren's shells:
su - warren
echo 'export PATH=/opt/local/bin:/opt/local/sbin:$PATH' >> ~/.zshrc
echo 'export PATH=/opt/local/bin:/opt/local/sbin:$PATH' >> ~/.bash_profile
exit
  1. Install the updated release from Northpole on GitHub.
  2. Grant permissions:
    • "Login Items & Extensions" > "App Background Activity" add Santa.app
    • "Login Items & Extensions" > "Extensions" > "By App" > toggle "Santa"
    • "Login Items & Extensions" > "Extensions" > "By Category" > "Endpoint Security Extensions" toggle Santa daemon
    • "Privacy" > "Full Disk Access" enable Santa Endpoint Security Extension (close and re-open Settings app after Santa install)
  3. Quit and re-open the terminal and check if Santa is running:
sudo santactl doctor
  1. Add a custom FAA policy:
sudo mkdir -p /var/db/santa
sudo vi /var/db/santa/faa_policy.plist
# Paste content

sudo chmod 644 /var/db/santa/faa_policy.plist
sudo chown root:wheel /var/db/santa/faa_policy.plist
  1. Download the Configuration Profile. Install this profile first before the mSCP config (section 4) because NIST configurations block adding new profiles.
cd ~/Desktop && mkdir Profiles && cd Profiles
vi santa.mobileconfig
# Paste content

sudo open santa.mobileconfig
  1. Debug:
# Force reload
sudo launchctl kickstart -k system/com.northpolesec.santa.daemon
sudo santactl status | grep "Policy Version\|Last Policy Update"
  1. Blocking application example (a selected list of banned apps are in the repo):
santactl fileinfo /System/Applications/Dictionary.app
# Path                   : /System/Applications/Dictionary.app/Contents/MacOS/Dictionary
# SHA-256                : 85f755c92afe93a52034d498912be0ab475020d615bcbe2ac024babbeed4439f
# SHA-1                  : 0cb8cb1f8d31650f4d770d633aacce9b2fcc5901
# Bundle Name            : Dictionary
# Bundle Version         : 294
# Bundle Version Str     : 2.3.0
# Signing ID             : platform:com.apple.Dictionary

# Use signing ID (will not change even with app update)
sudo santactl rule \
  --block \
  --signingid \
  --identifier platform:com.apple.Dictionary

santactl fileinfo /System/Applications/Dictionary.app
# Rule                   : Blocked (SigningID)
# Deprecated appproach: sha-256
sudo santactl rule --block/--remove --sha256 85f755c92afe93a52034d498912be0ab475020d615bcbe2ac024babbeed4439f
# Added/Removed rule for SHA-256: 85f755c92afe93a52034d498912be0ab475020d615bcbe2ac024babbeed4439f
  1. Certain files should be blocked by the FAA policy (because they should be immutable after being initialized), e.g.,
# Will be created in section 7
cat ~/.ssh/github_ed25519
  1. When importing/exporting rules, use:
sudo santactl rule --export santa1.json

3. mSCP Setup

Important

The NIST security compliance project does not modify any system behavior on its own. It generates a script that validates if the system reflects the selected policy, and creates a configuration profile that implements some changes.

Unless otherwise specified, all commands here should be ran at the project base (macos_security-*/).

  1. Download the repository and the provided YAML config in this repo, or a config from NIST baselines.
cd macos_security-tahoe/build
mkdir baselines && cd baselines && vi cnssi-1253_cust.yaml
# Paste content
  1. Ensure that the macos_security-* branch downloaded matches the OS version, e.g., macos_security-tahoe.
  2. Install dependencies, recommended within a virtual environment; after this step, warren will also gain paths to python3.14 and its corresponding pip package.
sudo port install python314
sudo port select --set python python314 && sudo port select --set python3 python314
# For admin's shell
echo 'export PATH=/opt/local/bin:/opt/local/sbin:$PATH' >> ~/.zshrc
echo 'export PATH=/opt/local/bin:/opt/local/sbin:$PATH' >> ~/.bash_profile
source ~/.zshrc
source ~/.bash_profile

python --version
# Python 3.14.2
python3 --version
# Python 3.14.2

sudo port install py314-pip
sudo port select --set pip pip314 && sudo port select --set pip3 pip314
rehash (zsh) / hash -r (bash)
pip --version
# pip 25.3
pip3 --version
# pip 25.3
cd ~/Desktop/Profiles/macos_security-tahoe
python3 -m venv venv && source venv/bin/activate
python3 -m pip install --upgrade pip && pip3 install pyyaml xlwt
  1. Small tangent: also check if MacPort libs also work in warren.
su - warren
python3 --version
# Python 3.14.2
python --version
# Python 3.14.2
pip3 --version
# pip 25.3 from /opt/local/Library
pip --version
# pip 25.3 from /opt/local/Library
exit
  1. Remove warren from the FileVault authorized users group to ensure that only admin can unlock FV (so during double-auth, the first login must be admin to unlock FileVault before logging in as warren):
sudo fdesetup list
sudo fdesetup remove -user warren
sudo fdesetup list
# Should only have admin
  1. Load custom ODVs (organization-defined values)
cd ~/Desktop/Profiles/macos_security-tahoe

cat > custom/rules/pwpolicy_minimum_length_enforce.yaml << 'EOF'
odv:
  custom: 12
EOF

cat > custom/rules/pwpolicy_account_lockout_enforce.yaml << 'EOF'
odv:
  custom: 5
EOF

cat > custom/rules/system_settings_screensaver_ask_for_password_delay_enforce.yaml << 'EOF'
odv:
  custom: 0
EOF
  1. Generate the configuration file (there should be a *.mobileconfig and a *_compliance.sh file). Note: do not use root for generate_guidance.py as it may affect non-root users. The python script will ask for permissions itself (repeat running the script even if it kills itself; it will eventually get all permissions it needs).
python3 scripts/generate_guidance.py \
        -P \
        -s \
        -p \
    build/baselines/cnssi-1253_cust.yaml
  1. If there is a previous configuration profile installed, remove it in Settings.app first. Run the compliance script.
sudo zsh build/cnssi-1253_cust/cnssi-1253_cust_compliance.sh
  1. First select option 2 in the script, then option 1 to see the report. Skip option 3 in this step. The compliance percentage should be around 18%. Exit the script.
  2. Install the configuration profile in the Settings app:
sudo open build/cnssi-1253_cust/mobileconfigs/unsigned/cnssi-1253_cust.mobileconfig
  1. After installing the profile, one way to verify that ODVs are working is to go to "Lock Screen" in Settings and check if "Require password after screen saver begins..." is set to "immediately", as this guide overwrites NIST guideline's default value for that field.
  2. Run the compliance script again (step 7) with options 2, then 1 in that order, i.e., always run a new compliance scan when settings changed. The script should now yield ~80% compliance.
sudo zsh build/cnssi-1253_cust/cnssi-1253_cust_compliance.sh
  1. Run option 3 and go through all scripts (select y for all settings) to apply settings not covered by the configuration profile. There are a handful of them.
  2. Exit the script first to ensure that sudo access persists (it is shortened in the new CNSSI profile).
  3. Run options 2 and 1 yet again. The compliance percentage should be about 96%. At this point, running option 3 will not do anything, because it does everything it can already, and the script will automatically return to the main menu.
  4. Run option 2, copy the outputs, and find all rules that are still failing. Usually it is these two:
os_firewall_default_deny_require
system_settings_filevault_enforce
  1. Go inside Settings and manually toggle these two options:
    • Enable "Filevault" under "Privacy and Security" > "Security". Wait until the encryption finishes.
    • "Block all incoming connections" in "Network" > "Firewall" > "Options". Further ensure that pf firewall and FileVault are enabled (ALF is enabled by default):
sudo bash includes/enablePF-mscp.sh

sudo pfctl -a '*' -sr | grep "block drop in all"
# Should output smt like "block drop in all" i.e. default deny all incoming
sudo pfctl -s info
# Should give output

# FileVault
sudo fdesetup status
  1. Note from previous step: one might encounter these two warnings.
    • "No ALTQ support in kernel" / "ALTQ related functions disabled": ALTQ is a legacy traffic shaping feature that has been disabled in modern macOS, which does not affect pf firewall at all.
    • "pfctl: DIOCGETRULES: Invalid argument": this occurs when pfctl queries anchors that do not support certain operations, but custom rules in this guide are still loaded (can still see block drop in all).
  2. The script should yield 100% compliance by running option 2, then option 1.
sudo zsh ~/Desktop/Profiles/macos_security-tahoe/build/cnssi-1253_cust/cnssi-1253_cust_compliance.sh
  1. Restart the device at this point.
  2. After restart, run the compliance script to verify that everything works. If system_settings_bluetooth_sharing_disable fails, it can simply be remediated by running option 3; or since it is already disabled in the Settings app, one can safely ignore it.

Note If unwanted banners show up, remove the corresponding files with sudo rm -rf /Library/Security/PolicyBanner.*

Note Run pip list after this section. There should only be pip and setuptools in global python environment.

4. AIDE Setup

  1. Install AIDE via MacPorts: sudo port install aide.
  2. Edit the configuration file:
sudo vi /opt/local/etc/aide/aide.conf
# Paste content
  1. Initialize database:
sudo aide --init -L info
#    INFO: read new entries from disk (limit: '(none)', root prefix: '')
#    INFO: write new entries to database: file:/opt/local/var/lib/aide/aide.db.new
#    ...
# AIDE successfully initialized database.
# New AIDE database written to /opt/local/var/lib/aide/aide.db.new
# Number of entries:	~9000
# End timestamp: ... (run time 0m 0-5s)
# INFO: exit AIDE with exit code '0'    <--- 0 = no error
  1. Move database into history directory & verify database:
sudo mv /opt/local/var/lib/aide/aide.db.new /opt/local/var/lib/aide/aide.db

ls -lh /opt/local/var/lib/aide/aide.db
# Expected: ~1.5MB
  1. Test installation:
sudo aide --check
# AIDE found NO differences between database and filesystem. Looks okay!!
# Number of entries:	~9000
...
# End timestamp: ... (run time 0m 0-5s)
  1. Test modifying watched directories:
sudo touch /Library/LaunchAgents/com.test.aide.plist

sudo aide --check
# AIDE found differences between database and filesystem!!
# Summary:
#   Total number of entries:	...
#   Added entries:		1
#   Removed entries:		0
#   Changed entries:		1

# ---------------------------------------------------
# Added entries:
# ---------------------------------------------------

# f++++++++++++: /Library/LaunchAgents/com.test.aide.plist
# Run whenever finished installing legitimate apps/processes
sudo aide --update
# New AIDE database written to /opt/local/var/lib/aide/aide.db.new
sudo mv /opt/local/var/lib/aide/aide.db.new /opt/local/var/lib/aide/aide.db

sudo aide --check
# AIDE found NO differences between database and filesystem. Looks okay!!

echo "modified" | sudo tee /Library/LaunchAgents/com.test.aide.plist
sudo aide --check
#  Added entries:		0
#  Removed entries:		0
#  Changed entries:		1
  1. Ensure that database exists:
ls -la /opt/local/var/lib/aide/aide.db
# -rw-------  1 root  admin  1454621 Jan 16 10:22 /opt/local/var/lib/aide/aide.db
  1. Cleanup after testing for the next manual/on-demand scan.
sudo rm /Library/LaunchAgents/com.test.aide.plist
sudo aide --update
sudo mv /opt/local/var/lib/aide/aide.db.new /opt/local/var/lib/aide/aide.db

sudo aide --check
# AIDE found NO differences between database and filesystem. Looks okay!!

A) Quick Reference

Task Command
Manual check sudo aide --check
Update after changes sudo aide --update && sudo mv /opt/local/var/lib/aide/aide.db.new /opt/local/var/lib/aide/aide.db
Re-initialize sudo aide --init && sudo mv /opt/local/var/lib/aide/aide.db.new /opt/local/var/lib/aide/aide.db
View log sudo cat /opt/local/var/log/aide/aide.log
Verbose check sudo aide --check -L info

B) Monitored Directories

Category Paths Reason
Startup /Library/LaunchDaemons, /Library/LaunchAgents Root-level persistence
GPG /opt/local/bin/gpg2, /opt/local/bin/gpg2-agent GPG integrity
Shell configs ~/.zshrc, ~/.bash_profile, etc. Backdoor detection
SSH /Users/*/.ssh/* SSH hijacking
PAM /etc/pam.d Auth bypass
User apps /Users/*/Library/Application Support/* Securing app data

5. DNS Setup

A) Hosts File

curl https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts | sudo tee -a /etc/hosts

B) DNSCrypt

Some VPN applications override DNS settings on connect; may need to reconfigure VPN and make it use the local DNS server (change DNS to 127.0.0.1). No need to configure DNSSEC in this step; it will be handled with Unbound.

  1. Install DNSCrypt with sudo port install dnscrypt-proxy. Do not load the port just yet, this will be done later.
    • Because there will be no Internet connection until the end of this section, also install Unbound with sudo port install unbound, and do not load it yet.
    • Also copy the DNSCrypt config and Unbound config beforehand.
    • Then, update DNS server settings to point to 127.0.0.1 ("Network" > Wi-Fi or Eth > Current network "Details" > DNS tab).
  2. Find DNSCrypt's installation location with port contents dnscrypt-proxy to get configuration files' path.
sudo vi /opt/local/share/dnscrypt-proxy/dnscrypt-proxy.toml
# Paste content
  1. Edit the property list to give DNSCrypt startup access:
sudo vi /opt/local/etc/LaunchDaemons/org.macports.dnscrypt-proxy/org.macports.dnscrypt-proxy.plist
<?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>Label</key>
    <string>org.macports.dnscrypt-proxy</string>
    <key>ProgramArguments</key>
    <array>
      <string>/opt/local/bin/daemondo</string>
      <string>--label=dnscrypt-proxy</string>
      <string>--start-cmd</string>
      <string>/opt/local/sbin/dnscrypt-proxy</string>
      <string>-config</string>
      <string>/opt/local/share/dnscrypt-proxy/dnscrypt-proxy.toml</string>
      <string>;</string>
      <string>--restart-netchange</string>
      <string>--pid=exec</string>
    </array>
    <key>Disabled</key>
    <false />
    <key>KeepAlive</key>
    <true />
    <key>RunAtLoad</key>
    <true />
  </dict>
</plist>
  1. Load the proxy:
sudo launchctl enable system/org.macports.dnscrypt-proxy
sudo port unload dnscrypt-proxy
sudo port load dnscrypt-proxy
  1. Check if current configuration is valid (will not run otherwise):
sudo /opt/local/sbin/dnscrypt-proxy -config /opt/local/share/dnscrypt-proxy/dnscrypt-proxy.toml -check
# Remember to reload dnscrypt-proxy after toml change

# If debug: run in foreground with verbose logging
sudo /opt/local/sbin/dnscrypt-proxy -config /opt/local/share/dnscrypt-proxy/dnscrypt-proxy.toml -loglevel 0
sudo lsof +c 15 -Pni UDP:54
# dnscrypt-proxy 57409 root    7u  IPv4 0xf2ce17b711151ccc      0t0  UDP 127.0.0.1:54
# dnscrypt-proxy 57409 root    9u  IPv6 0x8031285518513383      0t0  UDP [::1]:54
  1. After changing the network DNS resolver to use local, ensure that Wi-Fi interfaces use 127.0.0.1 instead of 192.168.x.x:
# Sometimes system will not respect GUI settings
sudo networksetup -setdnsservers "Wi-Fi" 127.0.0.1

networksetup -getdnsservers "Wi-Fi"
# 127.0.0.1

scutil --dns | head -10
# nameserver[0] : 127.0.0.1
  1. Again, since this guide routes dnscrypt-proxy to port 54, there will not be Internet connection until after section 2(C)

Note dnscrypt-proxy could take ~30 seconds to load on re-wake and startup, so there might not be connection immediately after session login.

C) Unbound

The original guide uses dnsmasq; however, Dnsmasq will not load ad (authenticated data) flag in DNS queries if an entry is cached. Hence this section is replaced with unbound to achieve both caching and auth.

  1. Unbound should already be installed in 5(B). If not, set DNS back to 192.168.0.1, install Unbound, and then change back to 127.0.0.1.
  2. Copy the configurations stored from 5(B) (here once again) into Unbound:
sudo vi /opt/local/etc/unbound/unbound.conf
# Paste content
  1. Initialize root trust anchor for DNSSEC.
sudo unbound-anchor -a /opt/local/etc/unbound/root.key
  1. Check configurations:
sudo unbound-checkconf /opt/local/etc/unbound/unbound.conf
sudo port load unbound
sudo lsof +c 15 -Pni UDP:53
# Should show unbound on 127.0.0.1:53 and [::1]:53
  1. Test Unbound dnssec:
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder

# First query - should have 'ad' flag
dig @127.0.0.1 dnssec.works

# Second query (cached) - should STILL have 'ad' flag
dig @127.0.0.1 dnssec.works
# ;; flags: qr rd ra ad;

# Third query - 'ad' flag should persist
dig @127.0.0.1 dnssec.works

# Test without DNS argument - should still go through 127.0.0.1#53 with `ad` flag
dig dnssec.works

# Test DNSSEC validation - should fail
dig @127.0.0.1 fail01.dnssec.works
# ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
unbound-host -vDr test.dnscheck.tools
# test.dnscheck.tools has address xx.xx.xx.xx (secure)
# test.dnscheck.tools has IPv6 address xx:xx:xx:xx:xx:xx (secure)
# test.dnscheck.tools mail is handled by 0 . (secure)

unbound-host -vDr badsig.test.dnscheck.tools
# ... (BOGUS (security failure))

Note Some websites will not have ad flag no matter how hard one tries. E.g.,

dig DNSKEY archlinux.org
# ;; Got answer:
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38272
# ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
#
# ;; QUESTION SECTION:
# ;archlinux.org.			IN	DNSKEY
#
# (!! No answer section !!)
#
# ;; AUTHORITY SECTION:
# archlinux.org.		3600	IN	SOA	hydrogen.ns.hetzner.com. dns.hetzner.com. 2026010201 86400 10800 3600000 3600
dig DNSKEY dnsviz.net
dig DNSKEY dnssec-debugger.verisignlabs.com
# ;; Got answer:
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: xxxxx
# ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
...

Note Debugging commands:

log show --predicate 'process == "dnscrypt-proxy"' --last 5m
curl -I https://google.com

# Test if resolver is blocking domains itself
dig @127.0.0.1 dnsleaktest.com
dig @9.9.9.9 dnsleaktest.com

One might have to quit and restart Safari (while testing) with killall Safari.

https://wiki.archlinux.org/title/Dnscrypt-proxy#Startup
https://00f.net/2019/11/03/stop-using-low-dns-ttls/
https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html https://wiki.archlinux.org/title/Unbound

6. Application Install

Note

Although this section applies to warren, it is more convenient to run the script inside admin because warren is not in the sudoers group (and hence cannot run sudo commands), and admin cannot read/write warren's files because admin is not root. This is the same note as in section 1.

When actually installing apps after following this guide, install BlockBlock, KnockKnock, and Little Snitch in this order. It prevents having to re-filter app binaries or miss binaries' persistence after the three tools are installed.

  1. Ensure that warren is not an admin (so apps should write to /Users/warren/Library/):
sudo dseditgroup -o edit -d warren -t user admin
# Should be blank
  1. Force certain /Library folders to be inaccessible to apps in ~/Library.
vi ~/Desktop/Profiles/directory_lock.sh
CRITICAL_DIRS=(
    "/Library/LaunchAgents"
    "/Library/LaunchDaemons"
    "/Library/StartupItems"
    "/Library/Security/SecurityAgentPlugins"
    "/Library/DirectoryServices/PlugIns"
)

for dir in "${CRITICAL_DIRS[@]}"; do
    sudo chmod +a "user:warren deny add_subdirectory,add_file,writeattr,writeextattr,delete,delete_child" "$dir"
done
sudo chmod +x ~/Desktop/Profiles/directory_lock.sh
~/Desktop/Profiles/./directory_lock.sh
  1. Note for step 2: one can add /Library/PrivilegedHelperTools into the directory list, but this will not be created until an application writes to that directory. I.e., this directory will be empty if one runs this script in a fresh system. Also test step 2:
ls -led /Library/LaunchAgents
ls -led /Library/LaunchDaemons
ls -led /Library/StartupItems

# drwxr-xr-x+ 3 root  wheel  96 Jan 11 14:30 /Library/LaunchAgents
#  0: user:warren deny add_file,delete,add_subdirectory,delete_child,writeattr,writeextattr
  1. Rollback command:
# use -a instead of +a
sudo chmod -a "user:warren deny add_subdirectory,add_file,writeattr,writeextattr,delete,delete_child" /Library/LaunchAgents
# ls -led /Library/LaunchAgents
# drwxr-xr-x  3 root  wheel  96 Jan 18 20:08 /Library/LaunchAgents
  1. When installing application for warren, make sure to create the directory /Users/warren/Applications (i.e., ~/Applications) and drag-and-drop apps there. The "Applications" folder on Finder's sidebar points to /Applications (i.e., root). As such, user apps will store files to ~/Library.

GnuPG

If any applications are curl'd through Git, make sure to configure it as in section 7(B).

Firefox

  • When using Firefox, use the uploaded user-overrides.js in this repo.
  • If macOS does not allow opening the LibreWolf browser, fix the error notification with xattr -d com.apple.quarantine /Applications/LibreWolf.app

VSCodium

To directly access VSCode's extensions marketplace:

vi ~/Applications/VSCodium.app/Contents/Resources/app/product.json
# Replace
  "extensionsGallery": {
    "serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
    "cacheUrl": "https://vscode.blob.core.windows.net/gallery/index",
    "itemUrl": "https://marketplace.visualstudio.com/items"
  },

7. Fish Shell

For this section, running in warren's GUI is easier (keep zsh as default for admin since that account should not be used besides global settings). Obviously su - admin when escalation is needed.

  1. First change the hostname:
sudo scutil --set ComputerName "device"
sudo scutil --set LocalHostName "device"
sudo scutil --set HostName "device"
hostname # device
  1. Restart the terminal to ensure the hostname change takes effect.
  2. Install fish through the admin account:
sudo port install fish
  1. Switch shells for warren only:
sudo sh -c 'echo /opt/local/bin/fish >> /etc/shells'
sudo chpass -s /opt/local/bin/fish warren
  1. Add paths to fish:
# su - warren
# fish (zsh is still default until quitting Terminal again)
fish_add_path /opt/local/bin
fish_add_path /opt/local/sbin
  1. Download Source Code Pro Nerd Font and macOS terminal config in this repo
    • There is nothing special about this terminal config, they are simply personal prefs).
    • Load the custom font and config in "Terminal" > "cmd + ," > "Import profile". This configuration chooses not to use iTerm2 because personally most features are not used, and not Alacritty because some of tide@v6's features do not work there.
  2. Install Fisher with the following extensions:
fisher install jethrokuan/z    # Example
  1. Download Node: nvm install lts.
  2. Add custom functions:
vi ~/.config/fish/functions/mkcd.fish

function mkcd
    mkdir -p $argv[1] && cd $argv[1]
end
vi ~/.config/fish/functions/up.fish

function up
  if test -z $argv[1]
    set n 1
  else
    set n $argv[1]
  end
  for i in (seq $n)
    cd ..
  end
end
vi ~/.config/fish/functions/uext.fish

function uext
    find . -type f | perl -ne 'print $1 if m/\.([^.\/]+)$/' | sort -u
end
  1. Install tide with fisher install IlanCosman/tide@v6 and add in custom configurations.
    • In the initial config, select "Classic" (step 1), "Dark" (step 3), and "24-hour format" (step 4). All other options can go with default.
    • Also edit the custom item context to complete this setup. Restart Terminal for both changes to take effect.
vi ~/.config/fish/config.fish
vi ~/.config/fish/functions/_tide_item_context.fish
  1. Install Amix's vim configuration:
git clone --depth=1 https://github.com/amix/vimrc.git ~/.vim_runtime
sh ~/.vim_runtime/install_awesome_vimrc.sh

A) SSH Configuration

  1. Install new configuration from ssh-config:
mkdir ~/.ssh
vi ~/.ssh/config
# Paste contents
chmod 600 ~/.ssh/config
  1. Create the sockets directory for multiplexing:
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
  1. Generate ED25519 key for GitHub:
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/github_ed25519 -C "github-$(hostname)-$(date +%Y)"
  1. Set permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/*_ed25519
chmod 644 ~/.ssh/*_ed25519.pub
  1. Add keys to macOS keychain:
ssh-add --apple-use-keychain ~/.ssh/github_ed25519
ssh-add -l
  1. Add SSH key to GitHub:
cat ~/.ssh/github_ed25519.pub | pbcopy
# Add to https://github.com/settings/keys as an authentication key, remove trailing new line
  1. Test SSH connection:
ssh -T git@github.com
cat ~/.ssh/known_hosts
# |1|qN7XE853AcGGBmJDT/APv+AiZGU=|qq21+AC5OMD...    <-- should be hashed
  1. Run the test script to verify that SSH settings are applied.
vi ssh_test.sh
# Paste content

chmod +x ssh_test.sh
./ssh_test.sh
rm ssh_test.sh

Note If legacy SSH servers are not working, use the following configuration:

Host legacy-server
  HostName old.server.com
  # Allow older key exchange for this specific server
  KexAlgorithms +diffie-hellman-group14-sha256,diffie-hellman-group14-sha1
  # Allow older ciphers
  Ciphers +aes128-cbc,aes256-cbc
  # Allow older MACs
  MACs +hmac-sha2-256,hmac-sha1
  # Allow older host key types
  HostKeyAlgorithms +ssh-rsa
  PubkeyAcceptedAlgorithms +ssh-rsa

B) GPG Configuration

  1. Make sure GnuPG is installed: sudo port install gnupg2.
  2. Create configuration directory and edit GPG configuration.
exit    # or su - warren
mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
vi ~/.gnupg/gpg.conf
# Paste content
  1. Give permissions and check for syntax errors:
chmod 600 ~/.gnupg/gpg.conf
gpg2 --gpgconf-test
# Should be empty
  1. Generate key (RSA and RSA):
gpg2 --full-generate-key
  1. List keys.
gpg2 --list-secret-keys --keyid-format=long
# sec   rsa4096/ABC123DEF456 2026-01-11 [SC]
gpg2 --armor --export ABC123DEF456
  1. Add the key to GitHub.
  2. Find the private enail address at "Settings" > "Access" > "Emails" > "Keep my email address private" and use that instead of the actual mailbox. Copy it to clipboard.
  3. Configure Git to use the signing key:
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
git config --global gpg.program gpg2
git config --global user.name "Yu-Jen Warren Wang"
git config --global user.email "private-email@goes.here"

Footnotes

Reboot and everything should work, even by directly logging into warren and not admin.

About

macOS setup with file/process allowlists, system integrity checks, mSCP profile, local DNS server, and SSH/GPG hardening

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published