Chapter 2. Design of the Job Framework

Table of Contents

Features
Capsicum

Features

This section describes the design of major features of the job framework.

Capsicum

Capsicum is a security mechanism that can be used to place programs within a sandbox. Prior to entering the sandbox, file and socket descriptors are acquired, and restrictions are applied to how those descriptors may be manipulated. Once the sandbox is entered, the program cannot interact with global namespaces like the filesystem or other processes, and may only use the descriptors that it already has opened.

The job framework has the ability to initialize the sandbox for programs that are Capsicum aware. This improves the overall security of programs which use Capsicum.

Capsicum traditionally is implemented by modifying the source code of a program to essentially sandbox itself when it executes. One benefit of applying Capsicum security policy within jobd is that the security policy can be viewed and audited by the end user or system administrator, without requiring them to look at the source code.

There is also no need to trust the person who wrote the program or compiled the program, as the security policy is in a separate document. You don't have to trust that the code you are running actually puts itself into a sandbox; instead, you can (hopefully) trust that jobd initializes the sandbox according to the policy that you can audit fairly easily.

For example, if someone put a backdoor in the source code of a daemon, having a separate security policy managed by jobd will limit the effectiveness of the backdoor to provide privilege escalation, because the daemon will be limited to access only those resources available to the sandbox.

In the future, it would be nice to have some kind of user interaction when the security policy of a program changes, similar to how smartphones ask you to confirm changes to app permissions when you upgrade to a new version of an app. This is totally possible as an extension to the job framework, but would be almost impossible to implement using the Capsicum API by itself.

To capsicumize a job, you will need to add two sections to the manifest: CreateDescriptors and CapsicumRights. In the CreateDescriptors section, you define what file descriptors should be created, and give each descriptor a unique name. In the CapsicumRights section, you apply rights(4) to each of these descriptors.

Here's an example:

{
  "Label": "mydaemon",
  "Program": ["/usr/local/sbin/mydaemon"],
  "User": "nobody",
  "Group": "nogroup",
  "ChrootDirectory": "/var/empty",
  "CreateDescriptors": {
      "kqueue": ["kqueue"],
      "resolv_conf": ["open", "/etc/resolv.conf", "O_RDONLY"],
      "mydata": ["open", "/var/mydata", "O_RDONLY"]
  },
  "CapsicumRights": {
      "kqueue": ["kqueue"],
      "resolv_conf": ["read", "sync", "event"],
      "mydata": ["create"]
  }
}

The reason that the CreateDescriptors is separate from the CapsicumRights is that multiple sandboxing mechanisms can share the same set of descriptors. The above example will acquire the descriptors prior to calling chroot(2), which is useful on platforms that do not have Capsicum.

When jobd starts the job, it would translate the above job manifest into something like the following code and execute it:


   int fd[3];

   fd[0] = kqueue();
   fd[1] = open("/etc/resolv.conf", O_RDONLY);
   fd[2] = open("/var/mydata", O_RDONLY);
   if (fork() == 0) {
        cap_rights_t setrights;

        cap_rights_init(&setrights, CAP_KQUEUE);
        cap_rights_limit(fd[0], &setrights);

        cap_rights_init(&setrights, CAP_READ | CAP_SYNC | CAP_EVENT);
        cap_rights_limit(fd[1], &setrights);

        cap_rights_init(&setrights, CAP_CREATE);
        cap_rights_limit(fd[2], &setrights);
      
        cap_enter();
      
        setenv("JOBD_DESCRIPTOR_kqueue", fd[0], 1);
        setenv("JOBD_DESCRIPTOR_resolv_conf", fd[1], 1);
        setenv("JOBD_DESCRIPTOR_mydata", fd[2], 1);
                
        execve("/usr/local/sbin/mydaemon", NULL, NULL);
    }

The 'mydaemon' executable would then be responsible for retrieving the file descriptors by examining the environment variables. This can be done using the job_descriptor_get() function. Here's an idea of what the 'mydaemon' program would need to do:


#include <err.h>
#include <stdlib.h>
#include <sys/event.h>

#if HAVE_JOB_DESCRIPTOR_H
#include <job/descriptor.h>
#else
#define job_descriptor_get(x) (-1)
#endif

int main(int argc, char *argv[])
{
    int kqfd;

    kqfd = job_descriptor_get("kqueue");
    if (kqfd < 0) {
        kqfd = kqueue();
        if (kqfd < 0) {
            err(1, "kqueue(2)");
        }
    }

    /* ... additional code omitted ... */
}

Notice how in the above example, the program needs to have a fallback plan in case that job_descriptor_get() fails or is not available. This is necessary to gracefully support building on operating systems that do not have jobd and/or Capsicum.