Supported non-admin service identity for a macOS LaunchDaemon

I'm developing a private macOS application on an Apple-silicon Mac mini running macOS Golden Gate 27.2.

I'm evaluating a system LaunchDaemon running under a dedicated, non-administrator identity that cannot log in interactively. A separate desktop application would communicate with the daemon through authenticated local IPC.

Before implementing this architecture, I would like to establish the officially supported approach.

My main question:

What is the Apple-supported method for creating and maintaining a dedicated, non-administrator, non-interactive service identity for a system LaunchDaemon on macOS 27.2?

In particular:

  1. Is there a documented procedure for creating, restricting, disabling, recovering and eventually removing this identity?
  2. Are dscl, sysadminctl or another account-management mechanism supported for this purpose?
  3. Can a LaunchDaemon run under this identity without an interactive user login, including when registered through SMAppService?
  4. Are there any secure-token, FileVault, volume-ownership or keychain restrictions that would affect this design?

If a dedicated non-login service identity is not a supported architecture, what alternative does Apple recommend?

I'd particularly appreciate links to official Apple documentation or guidance from Apple engineers.

Thank you.

Answered by DTS Engineer in 915636022

macOS’s support for role accounts is not particularly strong. There are (at least :-) four parts to this:

  1. Creating the role account.
  2. Using the role account in your launchd daemon.
  3. Handling the role account execution context.
  4. Keeping this running in the long term.

I’ll deal with each in turn.

1. Create the Role Account

The only way to specifically create a role account is by passing the -roleAccount flag to sysadminctl. This isn’t really an API — I mean, it doesn’t even have a man page )-: — but it’s the best we’ve got.

Another option is Identity Services, both the CBIdentity class within the Collaboration framework and the CSIdentity type within the OSServices framework. These let you create hidden users, and the doc comments for the underlying flag, kCSIdentityFlagHidden, make it pretty clear that hidden users are intended to be used for system services. Specifically, in <OSServices/CSIdentity.h> you’ll find this:

  /*
   * This flag causes the identity to be "hidden," that is, excluded
   * from most user-visible identity lists. Hidden identities include
   * administrative users and groups such as root, www, and mysql.
   * System service access control groups should be created with the
   * hidden flag.
   */
  kCSIdentityFlagHidden         = 1

However, the whole Identity Services concept is pretty much deprecated, either formally or informally.

2. Use the Account

Configuring a launchd daemon to use a role account is trivial: Set the UserName property in the launchd property list, as documented in the launchd.plist man page.

There are a couple of thinsg to watch out for here. First, we generally recommend that you install your launchd daemon using SMAppService, and that requires you to pass it the name of a launchd property list within your app’s bundle. That’s a read-only location, so you must know UserName in advance. And that means that, in the previous step, you have to pick a user name that’s not going to collide with any other product that does this. Usually that means embedding your product or company name into the user name.

Second, role accounts have a tendency to disappear. This might happen, for example, if the user re-installs the OS, or uses Migration Assistant to move from one Mac to the other. A product that uses one has to handle this. Typically that means detecting the problem and offering a way to repair it.

3. Beware the Execution Context

A launchd daemon always runs in the global context, as explained in TN2083 Daemons and Agents. If you customise the UserName property, the daemon ends up running in a mixed context. The user ID no longer matches the user ID associated with the context as a whole, which for the global context is, of course, root.

Running in a mixed execution context is tricky. You’ll find things that work and things that fail, and that list can change over time. For example, I recently learnt that enabling the App Sandbox in such a daemon doesn’t work.

4. The Long Term™

All of this means that role accounts tend to be brittle. You end up relying on a bunch of implementation details. As those can change over time, you have to update your product accordingly. So, it’s debatable whether role accounts are worth the effort on macOS. It might make more sense to run your daemon as root but limit its scope by enabling App Sandbox.

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

macOS’s support for role accounts is not particularly strong. There are (at least :-) four parts to this:

  1. Creating the role account.
  2. Using the role account in your launchd daemon.
  3. Handling the role account execution context.
  4. Keeping this running in the long term.

I’ll deal with each in turn.

1. Create the Role Account

The only way to specifically create a role account is by passing the -roleAccount flag to sysadminctl. This isn’t really an API — I mean, it doesn’t even have a man page )-: — but it’s the best we’ve got.

Another option is Identity Services, both the CBIdentity class within the Collaboration framework and the CSIdentity type within the OSServices framework. These let you create hidden users, and the doc comments for the underlying flag, kCSIdentityFlagHidden, make it pretty clear that hidden users are intended to be used for system services. Specifically, in <OSServices/CSIdentity.h> you’ll find this:

  /*
   * This flag causes the identity to be "hidden," that is, excluded
   * from most user-visible identity lists. Hidden identities include
   * administrative users and groups such as root, www, and mysql.
   * System service access control groups should be created with the
   * hidden flag.
   */
  kCSIdentityFlagHidden         = 1

However, the whole Identity Services concept is pretty much deprecated, either formally or informally.

2. Use the Account

Configuring a launchd daemon to use a role account is trivial: Set the UserName property in the launchd property list, as documented in the launchd.plist man page.

There are a couple of thinsg to watch out for here. First, we generally recommend that you install your launchd daemon using SMAppService, and that requires you to pass it the name of a launchd property list within your app’s bundle. That’s a read-only location, so you must know UserName in advance. And that means that, in the previous step, you have to pick a user name that’s not going to collide with any other product that does this. Usually that means embedding your product or company name into the user name.

Second, role accounts have a tendency to disappear. This might happen, for example, if the user re-installs the OS, or uses Migration Assistant to move from one Mac to the other. A product that uses one has to handle this. Typically that means detecting the problem and offering a way to repair it.

3. Beware the Execution Context

A launchd daemon always runs in the global context, as explained in TN2083 Daemons and Agents. If you customise the UserName property, the daemon ends up running in a mixed context. The user ID no longer matches the user ID associated with the context as a whole, which for the global context is, of course, root.

Running in a mixed execution context is tricky. You’ll find things that work and things that fail, and that list can change over time. For example, I recently learnt that enabling the App Sandbox in such a daemon doesn’t work.

4. The Long Term™

All of this means that role accounts tend to be brittle. You end up relying on a bunch of implementation details. As those can change over time, you have to update your product accordingly. So, it’s debatable whether role accounts are worth the effort on macOS. It might make more sense to run your daemon as root but limit its scope by enabling App Sandbox.

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

Hi Quinn,

Thank you for the detailed response. I’m evaluating the root-plus-App-Sandbox alternative you mentioned for separately running LaunchDaemon components on macOS Golden Gate 27.2.

I noticed that Apple’s “Signing a daemon with a restricted entitlement” documentation says App Sandbox is not appropriate for daemons, while your related discussion describes a sandboxed-daemon use case. Could you clarify:

  1. Supported configuration: For which daemon types, signing configurations and entitlements is App Sandbox supported or inappropriate? What supported signing, entitlement and launch configuration applies to the root-plus-App-Sandbox approach?
  2. Security isolation: Can separately running components be meaningfully isolated from one another with this approach, or is confinement primarily between each component and the wider system? What guarantees apply to credential custody, filesystem access, interprocess communication (IPC), and protection of a separate controller and authoritative data store?
  3. Startup behavior: What behavior is supported before FileVault unlock, after unlock but before user login, and after login?

For these points, which properties are documented guarantees, and which would need qualification on the target macOS version and hardware?

For context, I’m referring to your response here:
https://developer.apple.com/forums/thread/856568?answerId=915636022

The related discussion:
https://developer.apple.com/forums/thread/848726

And Apple’s daemon-signing documentation:
https://developer.apple.com/documentation/xcode/signing-a-daemon-with-a-restricted-entitlement

Thank you for your help.

I noticed that Apple’s Signing a daemon with a restricted entitlement documentation says App Sandbox is not appropriate for daemons

Things have changed a little since I wrote that [1]:

  • At the time, system extension were brand new and I didn’t understand the implications of packaging a Network Extension provider in a system extension, namely, that you end up with a system extension, which is essentially a daemon, that’s sandboxed.
  • That in turn revealed a number of problems, which we’ve fixed over the years. For example, the System keychain issue discussed on this thread.
  • Starting in macOS 14.2 we require that any daemon installed using SMAppService by a sandboxed app must also be sandboxed.

Given all of the above, I’m more tolerant of sandboxed daemons than I was previously. In contrast, my experience is that mixed execution contexts have become less reliable over the years. And hence the suggestion above.

If you file a bug against Signing a daemon with a restricted entitlement documentation and reply here with the bug number, I’ll get that updated.

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

[1] If you’re curious, that text originated in Packaging a Daemon with a Provisioning Profile, back in Feb 2020.