Seeking Recommended Approach for Sharing VPN Profile Private Keys Between Sandboxed macOS App and Packet Tunnel System Extension

Hello Apple Developer Community,

We are developing a full-tunnel VPN app for macOS that utilizes a packet tunnel network system extension (via NEPacketTunnelProvider). We're committed to using a system extension for this purpose, as it aligns with our requirements for system-wide tunneling. The app is sandboxed and intended for distribution on the Mac App Store.

Here's the workflow:

The app (running in user context) downloads a VPN profile from our server. It generates private keys, appends them to the profile, and attempts to save this enhanced profile securely in the keychain. The packet tunnel system extension (running in root context) needs to access this profile, including the private keys, to establish the VPN connection.

We've encountered challenges in securely sharing this data across the user-root boundary due to sandbox restrictions and keychain access limitations. Here's what we've tried so far, along with the issues:

Writing from the App to the System Keychain: Attempted to store the profile in the system keychain for root access. This fails because the sandboxed app lacks permissions to write to the system keychain. (We're avoiding non-sandboxed approaches for App Store compliance.)

Extension Reading Directly from the User Login Keychain: Tried having the extension access the user's login keychain by its path. We manually added the network extension (located in /Library/SystemExtensions/<uuid>/bundle.systemextension) to the keychain item's Access Control List (ACL) via Keychain Access.app for testing.</uuid> This results in "item not found" errors, likely due to the root context not seamlessly accessing user-keychain items without additional setup.

Using Persistent References in NETunnelProviderProtocol: The app stores the profile in the user keychain and saves a persistent reference (as Data) in the NETunnelProviderProtocol's identityReference or similar fields. The extension then attempts to retrieve the item using this reference.

We manually added the network extension (located in /Library/SystemExtensions/<uuid>/bundle.systemextension) to the keychain item's Access Control List (ACL) via Keychain Access.app for testing.</uuid> However, this leads to error -25308 (errSecInteractionNotAllowed) when the extension tries to access it, possibly because of the root-user context mismatch or interaction requirements.

Programmatically Adding the Extension to the ACL: Explored using SecAccess and SecACL APIs to add the extension as a trusted application. This requires SecTrustedApplicationCreateFromPath to create a SecTrustedApplicationRef from the extension's path.

Issue 1: The sandboxed app can't reliably obtain the installed extension's path (e.g., via scanning /Library/SystemExtensions or systemextensionsctl), as sandbox restrictions block access.

Issue 2: SecTrustedApplicationCreateFromPath is deprecated since macOS 10.15, and we're hesitant to rely on it for future compatibility.

We've reviewed documentation on keychain sharing, access groups (including com.apple.managed.vpn.shared, but we're not using managed profiles/MDM) as the profiles are download from a server, and alternatives like XPC for on-demand communication, but we're unsure if XPC is suitable for sensitive data like private keys during tunnel creation. And if this is recommended what is going to be the approach here.

What is the recommended, modern approach for this scenario? Is there a non-deprecated way to handle ACLs or share persistent references across contexts? Should we pursue a special entitlement for a custom access group, or is there a better pattern using NetworkExtension APIs? Any insights, code snippets, or references to similar implementations would be greatly appreciated. We're targeting macOS 15+.

Thanks in advance!

Answered by DTS Engineer in 855995022

The best path forward here is for the sysex itself to manage the credential. It’s effectively a launchd daemon and thus can [1] use the System keychain.

There’s a couple of ways you could spin this:

  • Have the app download your configuration [2] and then pass it to the sysex via XPC.
  • Have the app use XPC to instruct the sysex to download and save the configuration.

In both cases it’s the sysex accessing the System keychain, which avoids all the various issues you’ve bumped in to.

we're unsure if XPC is suitable for sensitive data like private keys during tunnel creation

XPC should be fine here. See XPC Resources for a lot of links to docs and so on.

Some things to note:

  • In the app, set the privileged flag [3] so that the system only looks for the named XPC endpoint in the global namespace. See this post for an explanation of that.
  • In the sysex, declare the named endpoint via NEMachServiceName.
  • In both the app and the sysex, claim access to an app group [4] and authorise that claim with a provisioning profile. See App Groups: macOS vs iOS: Working Towards Harmony for more about app groups in general.
  • Make the NEMachServiceName value a ‘child’ of that app group. App Groups: macOS vs iOS: Working Towards Harmony covers this point specifically.
  • In the sysex, check the identity of the client, as explained in Validating Signature Of XPC Process.
  • You can optionally have the app use the same technique to check the identity of the sysex, although that’s not really necessary if you set the privileged flag.

I think that’s everything, but please do let me know if you hit any snags.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] And must, per TN3137 On Mac keychain APIs and implementations.

[2] I’m not gonna use the word profile here because that’s already way too overloaded.

[3] XPC_CONNECTION_MACH_SERVICE_PRIVILEGED, NSXPCConnection.Options.privileged, and so on.

[4] In the app’s case this is an absolute requirement because the app is sandboxed and would not otherwise be allowed to connect to a named XPC endpoint.

The best path forward here is for the sysex itself to manage the credential. It’s effectively a launchd daemon and thus can [1] use the System keychain.

There’s a couple of ways you could spin this:

  • Have the app download your configuration [2] and then pass it to the sysex via XPC.
  • Have the app use XPC to instruct the sysex to download and save the configuration.

In both cases it’s the sysex accessing the System keychain, which avoids all the various issues you’ve bumped in to.

we're unsure if XPC is suitable for sensitive data like private keys during tunnel creation

XPC should be fine here. See XPC Resources for a lot of links to docs and so on.

Some things to note:

  • In the app, set the privileged flag [3] so that the system only looks for the named XPC endpoint in the global namespace. See this post for an explanation of that.
  • In the sysex, declare the named endpoint via NEMachServiceName.
  • In both the app and the sysex, claim access to an app group [4] and authorise that claim with a provisioning profile. See App Groups: macOS vs iOS: Working Towards Harmony for more about app groups in general.
  • Make the NEMachServiceName value a ‘child’ of that app group. App Groups: macOS vs iOS: Working Towards Harmony covers this point specifically.
  • In the sysex, check the identity of the client, as explained in Validating Signature Of XPC Process.
  • You can optionally have the app use the same technique to check the identity of the sysex, although that’s not really necessary if you set the privileged flag.

I think that’s everything, but please do let me know if you hit any snags.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] And must, per TN3137 On Mac keychain APIs and implementations.

[2] I’m not gonna use the word profile here because that’s already way too overloaded.

[3] XPC_CONNECTION_MACH_SERVICE_PRIVILEGED, NSXPCConnection.Options.privileged, and so on.

[4] In the app’s case this is an absolute requirement because the app is sandboxed and would not otherwise be allowed to connect to a named XPC endpoint.

Seeking Recommended Approach for Sharing VPN Profile Private Keys Between Sandboxed macOS App and Packet Tunnel System Extension
 
 
Q