Ever since people decided, around the UNIX days, that file access needed to be controlled, all mainstream operating systems have featured file access control policies that follow the same basic principles.
Every user of a computer has a unique numerical identifier (UID), belongs to one or more groups each having a similarly unique identifier (GID), and somewhere up in the sky there is a “super-user”, called root or Administrator, that can bypass file access control entirely whenever the need arises. Processes are run under the responsibility of a given user and group, and files are also owned by a user and group, by default that of the process which created them.
In this post, I will discuss why this security model has, in my view, outlived its usefulness in the area of general-purpose personal computing, and why I believe that process capabilities would be a better fit in this area.
The problem with users and groups
Although shared large computer systems still exist, for example in the area of high-performance computing or web hosting, they are not the preferred model for personal computing anymore. The computer that an average person needs for daily work and play is now cheap and small enough that it will usually have only one user, or occasionally a couple of users, when it is shared by a family for example.
In this situation, root access is much less of a big deal than it used to be. If you are the only user of your computer, then there is no fundamental reason for you to voluntarily create a limited user account, and force yourself into constantly switching between this limited account and the administrator one, other than masochism and limiting the amount of damage that inadequately sandboxed software can do. More on that in a moment.
Some operating systems acknowledge the limited usefulness of multiple users and groups on personal computers, by trying to repurpose them into a way to run processes at limited privilege. But this is, fundamentally, a very fragile hack. It turns brittle as soon as multiple human users interact with multiple process users; bends for large amounts of processes as file ACLs need to become impossibly complex and groups need to be shoehorned into a limited form of role-based access control; and ultimately breaks when external storage drives come into play, or when one actually tries to maintain the extremely intricate and interacting mess of users, groups, and ACLs that results.
On personal computers, access control is mostly about controlling what untrusted programs, not untrusted users and groups and users, can do. Current security infrastructures are unfortunately quite bad at acknowledging this.
There’s more to life than file access
Another problem with the aforementioned logic is that summing up system access control to reading, writing, and executing data in files turned out to be limitating surprisingly early, as early in fact as when someone came up with the idea of grouping files in directories.
A directory is, after all, very much a list of files, so reading and writing to that list could make sense, but what does executing a directory even mean? Well, someone thought they got that figured out, resulting in the infamous UNIX directory permission bit madness.
Then there was the problem of RAM access, where it was simply not viable to attach an ACL to every memory page around in order to declare which processes have the right to read, write, and execute them. In practice, this had to be handled using ad-hoc OS-specific mechanisms that are completely disjointed from the rest of the system’s security policy.
And then the functionality of operating systems grew far beyond what one can neatly express in terms of file access. For example, how would you express the action of taking a picture using a webcam in terms of file access permissions? How would you express that of sending data across the internet to a specific server using the TLS protocol? If you look at the tasks that modern applications do, you will quickly realize that expressing high-level process permissions in terms of file access permission is inadequate, because the data-centric file abstraction is only suitable for very low-level hardware interfacing and breaks down quickly for more complex operating system operations.
As a general rule, complex software development is made possible by abstraction, the implementation of high-level software logic on top of lower-level infrastructure. If the security infrastructure cannot follow this abstraction process by protecting software access to system commands, rather than hardware register data, it is bound to remain exceedingly low-level and broad in purpose, unable to express the finer-grained access control mechanisms that would be desirable these days.
Which brings us to…
The importance of process sandboxing
As I discussed before, on personal computers, controlling user access permissions is generally not that big of a deal. Give each user some private storage space that other users cannot read from, decide who gets the right to modify system-wide parameters and who doesn’t, and you get something that is reasonable for a typical shared family computer.
The main threat to personal computer security is not users sitting in front of the keyboard, but the untrusted software that these users run.
And no one can, at this point in time, prove that software is devoid of malicious intent:
- Too much software is released daily for each one to undergo a rigorous security audit, assuming that all developers would accept to release their source code to the experts conducting the audit.
- Automated binary analysis is famously unable to even tell if a program will terminate, so it is unlikely to ever be able to prove that a program cannot perform malicious actions.
- Any human software review process based on running an untrusted program in a controlled environment, such as a virtual machine, so as to check what it is doing, cannot prove that software will subsequently continue to behave the way it did during the review process.
For this reason, all personal computer software should be considered untrusted, and treated as potentially dangerous. Now, the way we usually deal with dangerous people or animals that we don’t want to kill has always been to strip them from their weapons and put them behind bars, and the same applies to software: when software is untrusted, it should be stripped from its nuisance potential to the fullest extent that is possible, while still allowing it to do its job.
That way of handling untrusted software is called sandboxing, and after years of general disdain from mainstream operating systems, it is finally slowly finding its ways into personal computers. Most newer mobile operating system designs, including Apple iOS, Google Android, Blackberry OS 10, and Firefox OS 2, feature some form of software sandboxing, though their implementation of this feature differs in several key user experience points:
- Some operating systems, like Android and Blackberry OS, take a proactive approach where software announces at installation time which resource access permissions it is going to need. Others, like iOS and Firefox OS, have programs request resource access permissions at the time where they are needed.
- Some operating systems, like Android, make resource access permissions a permanent contract between applications and users. Others allow users to revoke some application resource access permissions without uninstalling the application.
Each model has its pro and cons, and so far, there is no clear winner.
The Android approach has the advantage that no system dialog is displayed while applications are running, making it harder for applications to spoof such system dialogs in an attempt to trick users into giving them more privilege. But it leads to serious information overload at installation time if applications go overboard with resource access permissions, leading users to sometimes disregard the resource access part of the application installation process altogether.
The iOS and Firefox OS model is more fine-grained, but due to its reactive nature, means that users either have to click through a lot of resource access control warning, to the point where they ultimately stop reading them, or to permanently give applications access to some system components, in which case the design becomes similar to the Android model.
I find the Blackberry OS model particularly interesting. Applications request resource access permissions when they are first run, but the operating system makes a distinction between opt-in and mandatory permissions. This way, users are still aware of all the things the software are up to, but can disable all the optional functionality without breaking software by turning off vital resource access permissions.
Problems with access control lists
Now, as I mentioned above, process sandboxing is really much of a hack in current-generation operating systems. It’s taped together on top of legacy architectures that weren’t really designed for it, using finicky tricks like making one dedicated machine “user” per sandboxed process and adjusting its privilege at run time by making it join and quit purely artificial “groups”. Clearly, next-generation operating systems should be designed for it from the ground up, in order to bring the idea to its maximum potential without misusing old abstractions in tasks which they really weren’t designed for.
And in such an abstraction cleanup prospect, one has to ask whether access control lists really are the right tools for process sandboxing jobs.
In resource access control, one is trying to define the ability of actors, such as users and processes, to access resources, such as files. To do this, access control lists put emphasis on the resource being protected, whereas sandboxing puts emphasis on the actors being granted access to a resource. So even at a fundamental level, the two abstractions are based on fundamentally incompatible world views. If computer security was about sentences, then sandboxing would be about defining, for every sentence subject, which combinations of verbs and objects are allowed, whereas ACLs would be about defining, for every object, which combinations of subjects and verbs are allowed.
The emphasis that ACLs put on resources will often lead them to be stored alongside the resources that they are protecting, which is a source of problems in and of itself. Consider, for example, file access control on external storage drives. If file access permissions are stored on the storage drive itself, it means that whenever a drive is shared between two different computers, running two different OSs, each computer will overwrite the file access permissions in the way that matches its own worldview best, leaving the other computer very confused as for what just happened. Since data is moved around a lot more than software, one has to wonder whether it is a very good idea to store access permissions alongside data, rather than alongside the programs or users that access it.
Because they need to store data alongside resources, ACLs are, by their nature, very much file-centric. An ACL protecting anything other than a file has no natural storage location. If one wants to design an ACL for accessing a video output, for example, this ACL will have to be maintained and stored by the video driver, since there is no permanent storage space available on the video output device to store the access permissions. This leads to problems when video drivers with incompatible ACL storage locations are used, unless common standards for driver ACL storage and interchange are defined.
Current ACL implementations require special filesystems features. Simple filesystems such as the commonly used FAT family require ACLs to be stored in a dedicated database file, which subsequently has to be maintained in sync with every file operation that is preformed on the device. This causes problems, again, when an ACL-oblivious operating system manipulates an external storage drive that is shared with an ACL-aware one.
ACLs are also somewhat hostile to process authority delegation, and thus favor monolithic application design where programs have lots of security permissions at once. The reason for this is that ACLs provide no natural mechanism for processes to grant one another permission to do things, unless file ACLs are constantly tweaked or groups are constantly created to account for every single process interaction.
In short, if we wanted to fix all of the issues of modern-day ACL implementations, we would need to store ACLs as a dedicated database on the system storage drive, that is kept separate from the external hardware resources and transient software resources that the ACLs grant access to. Such centralized ACLs would also need to be resilient to resources appearing and disappearing from the external peripherals, and to remain light enough as to be quickly searchable whenever a program tries to access a resource. It should also be possible to constantly tweak them with very high performance to account for processes delegating authority to one another.
At this point, it starts to feel like what we’re looking for isn’t really an ACL anymore.
Capabilities as a natural sandboxing mechanism
Much like sandboxing itself, capabilities take the access control problem in a way that’s opposite from access control lists. At their heart, they are about defining, for every program, which resources it has access to, rather than defining, for every resource, which program has access to it.
A capability is defined as an unforgeable token containing a reference to a resource, and a set of actions that may be performed on that resource by the program holding the capability. The exact mechanism by which capabilities are made unforgeable is irrelevant, what matters is that capabilities cannot be modified by the process owning them, and can be passed around from one process to another when processes want to delegate their authority, for example when a high-level user process wants to delegate work to a lower-level system process.
Beyond this basic definition, capability-based access control systems can also be designed to have other properties. In our cases, two particularly relevant properties are capability persistence and revocability:
- Persistence means that a program can keep a saved copy of a capability and reuse it even as the specific process which received the capability is closed, and the program is later started again.
- Revocability means that a process which handed another process a capability can later choose to undo that operation, and destroy the capability for the process which received it as well as all the other process which the capability was subsequently shared with. It is vital when processes are not trusted, to prevent permanent breaches of security when a capability turns out to be inappropriate.
In an operating system implementing capability-based access control, resource access is managed at the program level. Programs which grant capabilities to other programs will usually keep track of which capabilities they have granted, so that they can later revoke them if the need arises. Whereas programs which receive capabilities keep track of which capabilities they have received, so that they can reuse them if the need arises. To avoid effort duplication, capability management libraries can be written, although the usual caveats of writing shared libraries apply.
Due to their decentralized nature, capabilities lend themselves well to abstraction. The reason for this is that nothing forces a process which receives a capability to transmit an identical capability to another process. Translations between high-level and low-level capabilities are possible, to the extent that available computing power will allow. So if we go back to our webcam picture example from earlier, the implementations could do it in the following way :
- A user application, having a webcam picture-taking capability, contacts the system video capture infrastructure, and requests a picture, passing the capability as proof of its authority.
- The video capture process checks the picture-taking capability, and looks up the hardware device which is currently set as the primary webcam. If there are several devices, it may prompt the user to pick one. If there are none, it can send an error back to the user application. Once it has found a suitable device, it contacts the device driver, and requests a video frame, using its own video device access capability.
- The webcam driver checks the video device access capability, and performs necessary low-level hardware initialization, capture request, and finalization. In doing so, it will need to contact lower-level system drivers, such as a USB bus driver, but none of the higher-level processes need to be aware of it.
- Finally, the video frame is sent up the stack, all the way to the user application.
Capability persistence is not always desirable. For example, applications need persistent access to their own configuration files, but they should not be granted persistent access to every user file which they once edited. This is where revocation comes into play. For example, a file access capability could come with the constraint that it may only be used once. In this case, once the application is done with a file and closes the associated file handle, the virtual filesystem process knows that the application should not be granted access to it anymore, and may safely revoke the associated capability.
Finally, capabilities address most of the aforementioned problems with ACL-based resource access control. Because they are naturally stored alongside programs, not resources, capabilities play very well with shared resources such as external storage drives, and with legacy filesystems without any fancy ACL storage features. And because they are unforgeable, capabilities do not strictly require management structures on the side of the process granting them. So two video output drivers accepting the same capability format could, in theory, be transparently swapped without voiding previously granted process video output permissions. However, it should be noted that things may become more tricky than this simplified example when revocation comes into play, depending on its implementation.
To conclude, capabilities are a decentralized, very general design allowing processes to grant one another permissions to do things. If they are made persistent and revokable, they are the most natural access control policy upon which one can implement process sandboxing, without suffering from many of the problems of resource-centric access control mechanisms such as ACLs.
Resource access control is a fundamental security concern of modern operating systems. Without it, any untrusted program or user could do whatever it wants with every file and system resource. This is not acceptable in a world where malware exists and cannot be easily told apart from harmless software.
Historically, operating system access control facilities have focused on the threat set by malicious users. However, on personal computers, this threat has all but vanished, whereas the threat posed by untrusted software has grown tremendously. This changing security climate calls for new security infratructures, setting an emphasis on process sandboxing rather than user control.
Access control lists have many problems, which already make them clumsy to use today. If they were repurposed for process sandboxing, the extra complexity would pose unbearable constraints on their implementation. A better approach appears to be a shift towards capabilities-based access control, which puts an emphasis on what software does, rather than on the resources that it works with.
Capabilities provide a natural abstraction for process sandboxing, at any level of the operating system service hierarchy. And given the additional properties of persistence and revocability, they can account for pretty much every sandboxing use case. They would thus be my resource access control policy of choice for implementing a modern personal computer operating system.