Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save cstrahan/d4eef25575492240a7d33e438baed96d to your computer and use it in GitHub Desktop.

Select an option

Save cstrahan/d4eef25575492240a7d33e438baed96d to your computer and use it in GitHub Desktop.

Enabling SSH on macOS for OrbStack Guest VMs

This guide documents how to enable SSH access from OrbStack Linux guest VMs to the macOS host. The standard "Remote Login" toggle in System Settings is necessary but not sufficient — macOS uses launchd socket activation for sshd which only binds to the loopback interface, making it unreachable from guest VMs on the OrbStack bridge network.

The solution is to run a second sshd instance via a custom LaunchDaemon that binds to all interfaces.

Prerequisites

  • macOS with OrbStack installed and at least one Linux VM
  • An SSH key pair on the guest (OrbStack shares the host's SSH agent by default)
  • The guest's public key in ~/.ssh/authorized_keys on the macOS host

Step 1: Enable Remote Login

Open System Settings > General > Sharing and enable Remote Login.

This activates the system com.openssh.sshd launchd service, which uses inetd-style socket activation. launchd holds the listening socket on port 22 and spawns sshd on demand. However, this socket only binds to the loopback interface, so only ssh localhost works.

Step 2: Configure SSH Access Control (SACL)

macOS uses a Service Access Control List (SACL) via the PAM module pam_sacl.so in /etc/pam.d/sshd. If the SACL denies the user, the connection is accepted and authenticated but then immediately closed — a confusing failure mode.

In the Remote Login settings, click the (i) button. You have two options:

Option A: Allow all users

Select "Allow access for: All users". This is the simplest option.

Option B: Allow specific users

Select "Allow access for: Only these users", then click + and add your user account. Equivalently, from the command line:

sudo dseditgroup -o edit -a YOUR_USERNAME -t user com.apple.access_ssh

You can verify membership with:

dseditgroup -o checkmember -m YOUR_USERNAME com.apple.access_ssh

Either option works. The key point is that the SACL must permit the connecting user, otherwise sshd will reject the connection after authentication with the log message:

Access denied for user ... by PAM account configuration

Step 3: Create a Custom LaunchDaemon

The system sshd service uses launchd socket activation (inetdCompatibility mode in /System/Library/LaunchDaemons/ssh.plist). This means launchd owns the listening socket and only binds to loopback. The system plist is SIP-protected and cannot be modified.

The workaround is to create a custom LaunchDaemon that runs sshd as a standalone daemon, managing its own sockets and binding to all interfaces:

sudo tee /Library/LaunchDaemons/local.sshd.plist << 'EOF'
<?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>local.sshd</string>
    <key>Program</key>
    <string>/usr/sbin/sshd</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/sbin/sshd</string>
        <string>-D</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
EOF

Key details:

  • -D: Runs sshd in the foreground (no daemonize), which is required for launchd to manage the process lifecycle.
  • KeepAlive: Restarts sshd if it exits unexpectedly.
  • RunAtLoad: Starts sshd at boot.
  • No inetdCompatibility: sshd manages its own sockets and binds to all interfaces (both 0.0.0.0 and :: on port 22).

Load the service:

sudo launchctl bootstrap system /Library/LaunchDaemons/local.sshd.plist

This persists across reboots.

Step 4: Verify

From the macOS host:

ssh localhost echo test

From an OrbStack guest:

ssh YOUR_USERNAME@host.orb.internal echo test

How It Works

OrbStack exposes the macOS host to guest VMs via host.orb.internal, which resolves to an IPv6 address on the OrbStack bridge network (e.g., fd07:b51a:cc66:f0::fe). The macOS bridge interfaces (bridge100, bridge101) carry this traffic.

The system sshd (via launchd socket activation) only listens on loopback, so connections from the bridge network never reach it. The custom LaunchDaemon runs sshd in standalone mode, which binds to all interfaces — including the OrbStack bridge — allowing guest VMs to connect.

Both sshd instances coexist: the system one handles loopback connections via socket activation, and the custom one handles connections on all interfaces.

Troubleshooting

"Connection closed" immediately after connecting: This is almost always the SACL. Verify with dseditgroup -o checkmember -m YOUR_USERNAME com.apple.access_ssh, or check the Remote Login settings in System Settings.

Diagnosing server-side issues: Run sshd in debug mode on a test port to see detailed logs:

sudo /usr/sbin/sshd -d -p 2201
# Then connect from the guest:
ssh YOUR_USERNAME@host.orb.internal -p 2201 echo test

Application Firewall: The macOS Application Firewall does not appear to affect this setup. In testing, connections from OrbStack guests succeeded regardless of whether /usr/sbin/sshd and /usr/libexec/sshd-keygen-wrapper were set to allow or block incoming connections in the firewall rules. The firewall likely does not filter traffic on the virtual bridge interfaces used by OrbStack.

Cleanup

To remove the custom sshd service:

sudo launchctl bootout system/local.sshd
sudo rm /Library/LaunchDaemons/local.sshd.plist
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment