Few months back , I have replaced my old Macbook pro with a tuxedo laptop and created a medium article about it .
Recently I decided to repurpose it into a remote server , so that I could access seamlessly via SSH from my primary linux machine , instead of letting it gather dust.
Here is the step-by-step story of how I configured it, the networking and OS-level hurdles I hit along the way, and how to solve every single one of them.
1. The Networking Decision between Static IPs , WireGuard and Tailscale
My first question was simple: Do I need to buy a static IP address from my ISP to SSH into my MacBook from outside my home network?
Thankfully, the answer is no. While home ISPs assign dynamic public IPs (and frequently lock connections behind CGNAT), you generally have three paths for remote access:
Dynamic DNS (DDNS) + Port Forwarding: Free services like DuckDNS map a domain to your changing IP, but you still have to open port
22on your home router—exposing your SSH daemon to constant automated brute-force bots.Raw WireGuard: WireGuard is an extraordinarily fast,open source VPN protocol. However, running raw WireGuard still requires a designated server endpoint, manual UDP port forwarding (
51820), and DDNS.Headscale vs. Tailscale: I briefly looked at Proxmox (which is a bare-metal hypervisor and doesn't solve the networking layer on its own) and Headscale (the open-source, self-hosted control plane for Tailscale). However, Headscale requires an external VPS with a public IP to coordinate handshakes anyway.
The Solution: I opted for Tailscale. It uses WireGuard under the hood as its data plane (meaning traffic is peer-to-peer encrypted), while acting as a zero-config coordination plane that punches through CGNAT and firewalls automatically. Best of all, Tailscale assigns each device a permanent 100.x.y.z IP address tied to its cryptographic node key—meaning my MacBook's virtual IP never changes, no matter where I take my client laptop.
2. Setting Up Remote Login & Hitting the First Roadblocks
With Tailscale installed on both my MacBook and my Linux laptop, I enabled macOS Remote Login via the terminal:
sudo systemsetup -setremotelogin on
Roadblock #1: Pinging the Public IP Fails
Initially, I tried pinging my MacBook’s public IP from my Linux machine to test connectivity and got 100% packet loss.
The Cause: Home routers drop incoming WAN ICMP (
ping) requests by default, and Tailscale doesn't route over public IPs anyway—it builds an isolated Layer 3 virtual network (utun).The Fix: Always test connectivity against the Tailscale IP (
100.x.y.z), not the public IP:
tailscale ping <mac_tailscale_ip>
Roadblock #2: No ED25519 host key is known for <ip> and you have requested strict checking
When I tried to SSH into the Mac's Tailscale IP for the first time, my Linux client immediately rejected the connection.
The Cause: My Linux SSH client had
StrictHostKeyChecking=yesenforced, refusing to connect to any host whose fingerprint wasn't already recorded in~/.ssh/known_hosts.The Fix: Override the check to automatically accept new host keys on initial handshake while keeping strict verification for future logins:
ssh -o StrictHostKeyChecking=accept-new <username>@<mac_tailscale_ip>
3. Hardening SSH: Why MAC Filtering Doesn't Work & Enforcing Key-Only Auth
At first, I considered adding MAC address filtering on my firewall for an extra layer of security. However, I quickly realized why that wouldn't work:
MAC addresses operate at Layer 2 (Data Link Layer): They do not survive router hops across the internet.
Tailscale is a Layer 3 (IP) tunnel: Its virtual
utuninterface encapsulates raw IP packets without Ethernet frames—meaning MAC addresses don't even exist inside a Tailscale tunnel.
Instead, the gold standard is Passwordless SSH (Ed25519 public-key cryptography) combined with completely disabling password logins.
Roadblock #3: Disabling PasswordAuthentication Isn't Enough on macOS
On standard Linux distros, setting PasswordAuthentication no in sshd_config usually suffices. On macOS, however, OpenSSH falls back to PAM via interactive keyboard prompts (KbdInteractiveAuthentication).
-
The Fix: After copying my Linux public key over (
ssh-copy-id), I created a drop-in configuration file at/etc/ssh/sshd_config.d/01-disable-passwords.conf(which survives macOS system updates):
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
Then, I validated and reloaded the macOS SSH daemon:
sudo sshd -t
sudo launchctl stop com.openssh.sshd
sudo launchctl start com.openssh.sshd
4. Principle of Least Privilege: Isolating a Standard user
Logging in repeatedly using my Mac's primary administrator account felt risky. If an automated script or exposed service were ever compromised, the attacker would have immediate sudo access. I wanted a dedicated, non-admin user (serveruser) exclusively for SSH.
Creating a Standard User via Terminal (dscl)
Rather than clicking through System Settings, you can provision a standard user (Group 20 / staff) directly from the terminal:
sudo dscl . -create /Users/serveruser
sudo dscl . -create /Users/serveruser UserShell /bin/zsh
sudo dscl . -create /Users/serveruser RealName "Server Service Account"
sudo dscl . -create /Users/serveruser UniqueID 502
sudo dscl . -create /Users/serveruser PrimaryGroupID 20
sudo dscl . -create /Users/serveruser NFSHomeDirectory /Users/serveruser
sudo dscl . -passwd /Users/serveruser
sudo createhomedir -c -u serveruser
Roadblock #4: No such key: GroupMembership When Restricting SSH Access
To lock down SSH so only serveruser could log in (and block my admin account), I tried reading the macOS SSH access group:
dscl . -read /Groups/com.apple.access_ssh GroupMembership
Which threw the error: No such key: GroupMembership.
The Cause: When Remote Login is enabled globally, macOS doesn't initialize the
GroupMembershipkey until individual users are explicitly added viadseditgroup.The Fix: Using
dseditgroupautomatically initializes the key and addsserveruser:
sudo dseditgroup -o edit -a serveruser -t user com.apple.access_ssh
sudo dseditgroup -o edit -d <admin_user> -t user com.apple.access_ssh
For defense-in-depth, I also created /etc/ssh/sshd_config.d/02-restrict-users.conf:
AllowUsers serveruser
Granting Scoped sudo Privileges
While serveruser shouldn't have full root privileges, it occasionally needs to reload firewall rules or adjust power settings. By creating /etc/sudoers.d/serveruser via sudo visudo -f /etc/sudoers.d/serveruser, I granted scoped, passwordless execution to specific binary paths only:
serveruser ALL=(root) NOPASSWD: /sbin/pfctl -f /etc/pf.conf, /sbin/pfctl -e, /sbin/pfctl -d
serveruser ALL=(root) NOPASSWD: /usr/bin/pmset -a disablesleep *
5. Locking Down Ports with the macOS Packet Filter (pf)
The built-in macOS Application Firewall in System Settings only blocks or allows applications globally—it cannot filter by source IP or port. To restrict incoming connections at the network packet level, macOS includes BSD's Packet Filter (pf).
Because my Linux laptop's Tailscale IP (100.x.y.z) never changes, I created a custom anchor at /etc/pf.anchors/custom.rules:
pass quick on lo0 all
pass out all keep state
# Allow SSH (and future custom app ports) strictly from my Linux Tailscale IP
pass in quick proto tcp from <linux_tailscale_ip> to any port 22 keep state
# Block all other incoming SSH attempts
block in quick proto tcp from any to any port 22
After referencing custom.rules inside /etc/pf.conf, I enabled it with sudo pfctl -ef /etc/pf.conf. If I ever spin up a web app on port 8080, Tailscale allows all ports between my devices by default—I simply add port 8080 to my pf anchor (or manage it centrally via Tailscale ACLs).
6. Preventing Sleep Mode from Dropping Connections
Roadblock #5: macOS Drops SSH When the Screen Sleeps or the Lid Closes
Even with everything configured, my SSH session disconnected after a few minutes whenever the Mac went idle or I closed the lid.
The Cause: macOS aggressively powers down network interfaces during idle sleep, and closing the lid triggers immediate system suspension unless an external display is attached.
The Fix: Use
pmsetto disable system sleep on AC power while allowing the display backlight to turn off after 5 minutes:
# Never sleep the system when plugged into AC power
sudo pmset -c sleep 0
# Turn off the screen after 5 minutes to save energy
sudo pmset -c displaysleep 5
# Keep network connections alive
sudo pmset -c tcpkeepalive 1
sudo pmset -c womp 1
And if you want to close the lid completely without attaching a dummy HDMI plug, override the lid switch:
sudo pmset -a disablesleep 1
7. The Final Boss: Running Docker Remotely Over SSH
Unlike Linux—where dockerd runs natively as a root systemd service—Docker on macOS runs inside a lightweight Linux VM managed by Docker Desktop, a graphical application. This created two major headaches over SSH.
Roadblock #6: open -a Docker Fails with Domain does not support specified action
When I tried launching Docker Desktop from my remote SSH terminal using open -a Docker, macOS threw a LaunchServices error.
The Cause: SSH sessions live in a headless background bootstrap domain, whereas
openrequires an active graphical WindowServer (Aqua) session.The Fix: Route the launch command into the active GUI user's bootstrap context using their UID (typically
501), or launch the binary directly in unattended mode:
# Find the active GUI user's UID
stat -f "%u" /dev/console
# Execute inside the GUI user's session
sudo launchctl asuser 501 open -a "Docker"
Roadblock #7: Docker Desktop is Running, But docker info Over SSH Says It Isn't
Even after starting Docker Desktop manually on the Mac, running docker info or docker ps over SSH as serveruser returned: Cannot connect to the Docker daemon... Is the docker daemon running?
The Cause: Modern Docker Desktop creates its UNIX socket inside the home directory of the GUI user who launched it (
/Users/<admin_user>/.docker/run/docker.sock) rather than exclusively using/var/run/docker.sock. Whenserveruserrunsdocker info, the CLI looks in/Users/serveruser/.docker/run/docker.sock, finds nothing, and fails.The Fix: Locate the active socket, grant read/write permissions so
serverusercan communicate with it, and exportDOCKER_HOSTinserveruser's~/.zshrc:
# 1. Grant access to the socket path (run as admin)
sudo chmod 755 /Users/<admin_user>/.docker /Users/<admin_user>/.docker/run
sudo chmod 666 /Users/<admin_user>/.docker/run/docker.sock
# 2. Point serveruser's Docker CLI to the active socket
echo 'export DOCKER_HOST="unix:///Users/<admin_user>/.docker/run/docker.sock"' >> ~/.zshrc
source ~/.zshrc
Conclusion
What started as a simple attempt to SSH into an old MacBook turned into a deep dive into macOS internals—covering launchd bootstrap domains, BSD pf firewalls, dscl directory services, pmset power assertions, and UNIX domain sockets.
Today, my old MacBook sits quietly on a shelf: screen off, unreachable from the public internet, locked down to a single non-admin user via Ed25519 keys, and running Docker containers smoothly whenever I connect over Tailscale from my Linux laptop.
Top comments (0)