Access Control

Thinking about Security

Paul Krzyzanowski

September 17, 2020


Protection is an essential aspect of secure systems. By protection, we refer to the mechanism that provides and enforces controlled access of resources to processes. To protect resources, we need to first authenticate the user. Authentication is the process of getting and validating a user’s identity. Then we can authorize the user’s access to a desired resource. Authorization determines whether an authenticated user is permitted access to a resource. This will be based on some configured security policy.

Processes are run by users, so the mechanism enables users to have varying levels of access to resources, an ability to run specific programs, and in some cases, run programs with privileges other than those of the user running the program. A protection mechanism enforces security policies. The policies are the definition of what is or is not permissible in the organization.

At the operating system level, protection encompasses several components:

  • User accounts: Users need to identify themselves and be authenticated so the system can have assurance of the integrity of the user and apply appropriate policies.

  • User privileges: Each process run by the system or by a user runs with specific privileges that define access rights – what resources the process can and cannot access. In most cases, these resources are files but they can be devices or communication interfaces[1].

  • Scheduling: the operating system is responsible for scheduling processes and may give certain users higher priorities than others.

  • Quotas: the system may impose quotas: how much disk space, CPU, or network traffic a given process or user is allowed.

The very earliest computers did not have a need for access control. They were single-user batch-processing systems. Both the program and data were physically brought to the computer, usually in the form of punched cards and magnetic or punched paper tape. Once disks came onto the scene, computers had always-available files on them. Now, privacy (confidentiality) and integrity were concerns. You may not want somebody else reading your files and you do not want others modifying your files unless you give them explicit permission to do so. In short, you want to protect yourself from curious users, malicious users, and from their mistakes.

Later, interactive timesharing systems dominated, and we had an environment where multiple processes from multiple users ran at the same time, making casual inspection of files easier and allowing multiple processes to access the same file at the same time. Malicious software, for example, could keep a file open and monitor its changes – or make changes. Access control was needed. We needed operating system mechanisms that would enforce defined policies on who could access what.

When PCs took over the world, access control took a back seat for a while. We were back to single-user systems: every file is yours. However, we soon realized that software became less trusted. You now had to worry about whether that game you installed would modify your system files or upload your private files to a remote server. It turned out that access control was still important. Now we live in a world of PCs, mobile devices, IoT[2] devices, as well as remote servers – cloud computing – where we share computers and storage. We also have traditional time sharing systems, such as university computers and corporate servers. Program isolation and access control is still crucial.

Access control

Access control is about ensuring that authorized users can do what they are permitted to do … and no more than that. In the real world, we rely on keys, badges, guards, and policy rules for enforcing access control. In the computer world, we achieve access control through a combination of hardware, operating systems, web servers, databases, and other multi-access software, and polices.

Access control and the operating system

In its most basic sense, an operating system is responsible for controlling access to system resources. These resources include the processor, memory, files and devices, and the network. The operating system needs to be protected from applications. If not, an operation may, accidentally or maliciously, destroy the integrity of the operating system. Applications also need to be protected from each other so that one application cannot read another’s memory (enforce confidentiality) or modify them (enforce integrity). We also need to make sure that the operating system always stays in control. A process should not be able to take over the computer and keep the operating system or other processes from running.

Hardware timer

To make sure the operating system can always regain control, it relies on a programmable hardware timer. Traditionally, the operating system would request a periodic stream of interrupts, such as 100 per second. In an effort to be more power efficient, some operating systems now replace the periodic stream of interrupts by setting the timer to go off at a specific future interval. In either case, when the timer goes off, it generates an interrupt that forces the processor to switch execution to preprogrammed location in the kernel. This ensures that the operating system can always regain control of the processor and a process cannot keep the operating system from doing so. This is the basis of preemptive multitasking. Some systems in the past, such as the earliest versions of Microsoft Windows (before Windows 95), implemented cooperative multitasking, where a process had to relinquish control explicitly.

Process scheduler

The timer forces a periodic change of control to the operating system kernel. This allows it to examine processes in execution and make scheduling decisions. Specifically, the operating system decides whether the currently running process(es) used their fair share of the CPU and it is time to give another process the chance to run for a while. An important part of the scheduling algorithm is to avoid starvation, the phenomenon when certain processes never get a chance to run. If it were possible for a process to keep others from running, that would be an availability attack. The scheduler attempts to prioritize threads based on some concept of “fairness”. This priority could be based on the user, user-defined priority, interactivity of the process, real-time deadlines, or past use of the processor. Often, it is a combination of these factors. A “fair” scheduler will provide graceful degradation of performance as more processes are run, ensure all get a chance to run, and not allow one process to adversely affect the execution of others.

Memory management unit

The majority of modern processors[3] have a memory management unit (MMU) as part of their architecture. This enables the kernel to provide each process with virtual memory, the illusion of a complete private memory address space. A valid memory reference gets translated into a physical address in real memory. Because each process has its own address space (defined by a per-process page table), one process cannot access another process’ memory. With this mechanism, the operating system can provide complete memory isolation between processes. The virtual memory address space is divided into equal-size chunks, called pages and the operating system can define access rights for each page, such as read-only, read-write, or execute.

Kernel mode

Clearly there are special operations that the operating system needs to perform. These include setting timers, configuring the page table for each process, and switching processes. Normal processes should not be permitted to do any of these operations. To support this dichotomy, processors support a special mode of execution called kernel mode (also called privileged, system, or supervisor mode). This mode called kernel mode because it is used by the operating system kernel: processes run in user mode. When the processor runs in kernel mode, it can access regions of memory that may have been restricted, modify the page tables and the processor’s page table register, set timers, define interrupt vectors (e.g., direct where CPU execution should go if a timer goes off or an ethernet packet is received), and even halt the processor.

Normal processes, even those with elevated privileges, should not be allowed to access these special instructions since it would allow them to subvert the integrity of the kernel or other processes. They run in user mode. A process can switch from user mode to kernel mode execution in one of three ways:

  1. A trap instruction, which is also called a software interrupt[4]. This is the technique used to execute system calls.

  2. A program violation, such as accessing an unmapped are area of memory or attempting to execute code from a memory location that does not have execute privileges.

  3. A hardware interrupt, such as a timer expiration, power button press, or the receipt of a packet.

Rings of protection

Figure 1. Rings of protection<br/>from
Figure 1. Rings of protection

Today’s operating systems support these two modes of operation: user and kernel. Kernel mode has all the privileges while user mode has restricted privileges. However, there is no reason that only two modes of privilege need to exist.

When Multics, the predecessor of Unix, was designed, it had a ring structure of six[5] different privilege levels. Each ring was protected from higher-numbered rings and the only way code running in a higher-numbered ring could access code in a lower-numbered ring was by making a special call to cross rings. These calls, with well-defined entry points, are called call gates. Only the lowest-level, most-secure functions ran in ring 0. The rest of the kernel ran in higher rings and user processes ran in even higher rings.

The Intel x86 architecture supports four privilege levels – four rings. However, modern operating systems only use two of them: ring 0 for the kernel and ring 3 for user applications.

Subjects and objects

In order to determine who gets to do what, the first thing that the operating system needs is the user’s identity. Typically, the login program establishes that by getting the user’s credentials, which usually comprise a login name and password and authenticating them. The system then associates a unique user ID with that user and grants access to resources based on that ID.

When we look at access control, we talk in terms of subjects and objects. A subject is the thing that needs to access the resources, or objects. Often, the subject is the user. However, the subject can also be a logical entity. For example, you might run the postfix mail server and create a user ID of postfix for that server even if it does not correspond to a human user. Having this distinct ID will enable you to configure access rights for the postfix server that are distinct from other users. You will also likely do this for certain other servers, such as a web server.

An object is the resource that the subject may access. The resource is often a file but may also be a device, communication link, or even another subject.

Access control defines allowable operations of subjects on objects: it defines what different subjects are allowed to do or defines what can be done to different objects.

Most operating systems define what can be done with different objects, meaning that permissions are associated with each object.

The Unix (POSIX) access model

Access isn’t an all-or-nothing proposition. You may have access to read a file but not to write it, or access to read or execute a file. Each object can have different permissions associated with it. We will first look at how POSIX systems manage permissions. This includes the set of systems that have been derived from or inspired by the original Bell Labs version of Unix, including Linux, Oracle Solaris, FreeBSD, NetBSD, OpenBSD, macOS, Android, iOS, and many other lesser-known systems.

Access and sharing files

In a shared environment, you’d like to be able to set access rules so that multiple people, say members of a project, can share access to the same file. Each file (object) has an owner and a group associated with it. Each user (subject) has a unique user ID and may belong to one or more groups.

File permissions are expressed as


where the first rwx group represents read, write, and execute permissions for the user (owner), the second rwx group represents read, write, and execute permissions for the group, and the last rwx group represents read, write, and execute permissions for everyone else. For example, running the ls command with the “long” flag to inspect /usr/bin/ls (the file containing the ls command) shows:

$ ls -l /bin/ls
-rwxr-xr-x  1 root  wheel  38624 Dec 10 04:04 /bin/ls

This shows that the file is owned by the user root and belongs to the group wheel. Access permissions are {read, write, execute} for the owner and {read, execute} for the group and for everyone else. The leading - is not a permission flag but indicates whether the file is a plain file (-), directory (d), block device (b), character device (c), or named pipe (p).

The kernel processes access permissions only when the file is first opened and in the following order:

if you are the owner of the file
    _only_ the owner permissions apply
else if you are in the group that the file belongs to
    _only_ the group permissions apply
    the "other" permissions apply

Hence, if I have the following file:

----rw----  1 paul  localaccounts  6 Feb  4 19:15 testfile

I cannot read it even though I am a member of the localaccounts group and that group has read-write access to the file.

Execute permission

Note that execute permission is distinct from read permission. For example, you may have execute-only access to a file

$ ls -l secretfile
-rwx--x--x  1 root  staff  8492 Feb  4 19:22 secretfile
$ cat secretfile
cat: secretfile: Permission denied

and not be able read it but the operating system will load it and execute it if you run it. By losing read access to the file you lose the ability to copy it or inspect its contents.

Microsoft Windows

Windows provides the same read, write, and execute permissions but also adds specific per-file permissions for delete, change permission, and change ownership. Moreover, users and resources can be partitioned into domains. Each domain can have its own administrator. For instance, the human resources department may manage users while individual departments may manage connected devices, such as printers. Trust can be configured to be inherited in one or both directions. Department domains may trust the user domain but the user domain may choose not to trust the department resources domains.

Unix directories

Directories are implemented as files. They just happen to be files that contain a list of {name, inode number} pairs. Recall that an inode number is the data structure in a file system that contains all the information about a file except for its name[6].

However, directory permissions have slightly different meanings:

  • write permission means that you have rights to create and delete files in the directory.
  • read permission means that you have rights to list the contents of a directory
  • execute permission means that you have rights to search through the directory. This differs from read permission because you cannot see what files are in the directory; the operating system is simply willing to access the directory to resolve a pathname.

We can see that directory permissions can give you permission to search through the directory when opening files even if you cannot access the contents. If you have write access to a directory, you can delete any file within that directory, even if you do not have write access to the file itself. Conversely, if you do not have write access to the directory, you cannot delete any file within it, even if you have write access to the file. Your option could be to open the file and destroy the contents by overwriting them or truncating the file, but you cannot make the file itself go away.

User and group IDs

On most POSIX systems[7], user ID information, except for the password, is stored in the password file, /etc/passwd. This file contains lines of text, one line per subject, with each line containing the following fields:

  • User name
  • User ID
  • User’s default group ID
  • User’s full name
  • Home directory
  • Login shell

For example:

root:*:0:0:System Administrator:/var/root:/bin/sh
paul:x:1000:1000:Paul Krzyzanowski:/home/paul:/bin/bash

Group IDs are stored in a file called /etc/group. Each line contains a group name, unique group ID number, and a list of user IDs that belong to that group:


Changing permissions

The chmod command (or the chmod) system call allows you to change permissions for a file or directory. The command lets you explicitly specify permissions for the user (owner), group, and other. For example, you can set permissions:

$ chmod u=rwx,g=rx,o= testfile
$ ls -l testfile
-rwxr-x---  1 paul  localaccounts  6 Jan 30 10:37 testfile

Add permissions (in this case, we’re adding write access to group and other):

$ chmod go+w testfile
$ ls -l testfile
-rwxrwx-w-  1 paul  localaccounts  6 Jan 30 10:37 testfile

or remove permissions (take away write access from other):

$ chmod o-w testfile
$ ls -l testfile
-r-xrwx---  1 paul  localaccounts  6 Jan 30 10:37 testfile

The earliest versions of the command only accepted a bitmap for the permissions you want to set: one octal digit (0–9) for each of the three three-bit groups. The system call requires that and the command still accepts it:

$ chmod 754 testfile
$ ls -l testfile
-rwxr-xr--  1 paul  localaccounts  6 Jan 30 10:37 testfile

Access control lists

Sometimes groups are not enough and we may want to enumerate access permissions explicitly over a set of users or groups. This list of access rights for a file is called an access control list. An access control list comprises a list of access control entries (ACEs). Each ACE identifies a user or group along with the access permissions for that user or group. Typically, these permissions are read, write, execute, and append for files and list, search, read attributes, add file, add sub-directory, and delete contents for directories. A directory may also have an inheritance attribute, which specifies that files created within that directory will inherit specific access permissions (and directories created underneath that directory may inherit another set of access permissions). This helps with the problem of having to create many identical access control lists for a collection of files. Access control entries often also allow the use of wildcards (e.g., “*”) to refer to all users or all groups. For example, consider the following ACL:

 pxk.*         rwx  
 419-ta.*      rwx  
 *.faculty     rx  
 *.*           x

Access control entries are processed in sequential order. Users pxk and 419-ta have read, write, execute access to the file. Anyone in the faculty group has read, execute access and everyone else only has execute access.

If the order was changed:

 419-ta.*      rwx  
 *.faculty     rx  
 pxk.*         rwx  
 *.*           x

and user pxk belonged to the faculty group, that user would no longer have write access to the file because *.faculty has precedence over pxk.*.

For an example of setting access controls, see the manual page for chmod on macOS or setfacl on Linux.

The reason Unix systems (including Linux) did not originally implement full access control lists is because of space. All file attribute data is stored in a fixed-length chunk of data in the file system called the inode. The inode contains information such as the file’s owner, group, create time, last modification time, last access time, size of file, and the blocks in the file system that contain file data. The beauty of the user-group-other mechanism is that it takes a small, fixed length of data (two bytes for the user ID, two bytes for the group ID, and two bytes for the permissions bit mask). To support a true access control list, we need to have a variable size list. Most operating systems and file systems have evolved to support extended attributes: file attributes that do not fit into the inode structure (e.g., file download URL). These attributes are stored in extra blocks in the file system and are also used to store an access control list.

Extended attributes often give permissions beyond basic read, write, and execute. On POSIX systems, for example, the include:

Operations on all objects:
delete, readattr, writeattr, readextattr, writeextattr, readsecurity, writesecurity, chown
Operations on directories:
list, search, add_file, add_subdirectory, delete_child
Operations on files:
read, write, append, execute

They also include attributes to control inheritance: to define default ACLs for files in subdirectories.

In systems such as Linux, which support both ACLs and the simple user-group-other permissions, the following search order applies:

  1. If you are the owner of the file, only owner permissions supply.
  2. If you are part of a group that the file belongs to, then only group permissions apply.
  3. Search through the access control list entries to find an applicable entry. The rules of the first matching entry apply.
  4. Otherwise, “other” permissions apply.

Initial permissions

On POSIX systems, each process has a file mode creation mask associated with it. It is called a umask and is set to a bit pattern that reflects the rwxrwxrwx permissions. In this mask, however, each 1 bit represents a permission that will be disabled from the file. The umask is applied prior to creating the file to strip away permissions that might have been set by the user program. For example, we might have code to create a file with read-write permissions for everyone (rw-rw-rw-):

int f = creat("testfile", 0666);

If our umask is 022 (binary = 000 010 010 = — -w- -w-), then the kernel’s application of the umask will strip away write permissions for the group and others, resulting in final file permissions of rw-r–r–. A process can change the value of the umask via the umask system call. A user can change the value via the shell umask command, which will apply that umask to all commands executed from the shell.

Why is the umask important?

It provides a some safeguards: if a program is careless about setting access permissions, umask ensures that a reasonable default is applied (e.g., no write access for anyone but the owner). More importantly, for scripting languages that do not allow us set permissions when a file is created (e.g., perl, bash), the umask mechanism gives us a way to set defaults. For instance, when you redirect output to a file, you have no way of setting permissions on the resulting file except by defining a umask a priori. One could argue that you can set permissions after the file is created. For example:

./program > output
chmod 600 output

but that introduces a race condition that can open the file to attackers. If an attacker opens the file after it is created but before its permission bits are changed, she has access to the file even if access permissions change. Access rights are checked only when the file is opened.

Changing file ownership

User and group identities are not fixed. A user can give a file away via the chown command (or chown system call):

chown alice testfile

Similarly, the group of a file may be changed as well:

chgrp accounting testfile

The chown system call is also used for changing groups from within a program.

Changing user and group IDs

When a POSIX (e.g., Linux) system boots up, the first process[8] runs with root privileges. Root is a special user (user ID = 0) that has administrator privileges. File access permissions do not apply to root, who can read, write, or delete files even if access permissions do not grant that privilege. Root users also have access to certain restricted system calls.

How do processes start with other IDs? For example, how does your shell run with your user ID and your group ID? A process running as root can change its user ID or group ID to that of another user/group with the setuid and setgid system calls. A login process, for example, runs as root and:

  1. Requests your credentials: your user name and password
  2. Validates those credentials to authenticate you
  3. Once you’re authenticated, the login program reads your user ID, group ID, home directory, and login program (login shell) from the /etc/passwd file.
  4. The the login program now changes ownership and launches the shell:
    1. Change to the user’s home directory: chdir(user_home_directory);
    2. Change to the user’s group ID: setgid(user_group_id);
    3. Change to the user’s user ID: setuid(user_id);
    4. Execute the user’s login program (usually a shell). On POSIX systems, execution does not spawn a new process (fork does that) but overwrites the code in the current process: execve(user_shell, argv, envp);

User ID change on program execution

When a user runs a program, the program runs with that user’s user ID and hence with the privileges of that user. Sometimes, however, a program may need special access to files, devices, or system calls that the user of the program should not be allowed to access. For instance, a database program reads and writes files that store the underlying data but it is not a good idea to allow users of the database to be able to access those files directly. On some systems, network programs such as ping may need to create raw sockets yet normal users are restricted from doing so.

To address these needs, executable files can have a special permission bit set called setuid. When set, the executable file will run with the user ID set to the owner of the file rather than the user that is invoking the program. This user ID becomes the effective user ID of the process.

The setuid bit can also be used on directories. If a directory has its setuid bit set, all files and sub-directories created within it will be owned by the directory owner rather than the user who created them.

A related access permission bit, called the setgid bit causes programs to run with the effective group ID set to the group ID of the file.

Setuid programs are particularly attractive targets because they allow an intruder to gain privileges of another user, usually the administrator. If a setuid program can be co-opted to execute some other code or access a file that the intruder wants, it may provide a way for the intruder to run arbitrary administrative commands on the system or access arbitrary data.

Principle of Least Privilege

An important design principle when building secure systems is the principle of least privilege, which state that

At each abstraction layer, every element should have access only to the resources necessary to perform its task

An element refers to either a user, process, or a function. It means that even if an element is compromised (and must be paranoid and assume that is possible), the scope of damage will be limited. Some examples of the principle in action are:

  • You have the right to kill processes, but only your own. If intruders gains your identity, they will not be able to destroy processes throughout the system.

  • You can read the /etc/hosts file, which contains system overrides for name to IP address mappings but you cannot write it.

  • When you write a program, private member functions are not accessible outside the class. A poorly written program cannot casually invoke these functions.

Some examples that violate the principle are:

  • A ping program runs as setuid to root since it needs to access raw sockets. Because of this, if it is compromised, a user can do anything on the system.

  • A program uses global variables even through only one function ever touches these variables. It creates the risk that other functions may improperly access those variables.

  • Default permissions on a file set to read-write for all introduces the risk that people who have no business touching the file will modify it.

  • A mail server running with root privileges because it needs to listen on port 25[9] but that gives it access to all other files and devices on the system as well as to privileged system calls.

Privilege separation

Least privilege can be tricky to implement since it requires a thorough understanding of how a program behaves. Moreover, running a process as root is sometimes necessary but imbues an application with incredible power that can be abused. To get around this issue, we can use privilege separation. The principle of privilege separation is to break the program into multiple parts. Each part runs with only the privileges it needs to perform its task. If one part becomes compromised, potential damage is limited to only that component. More importantly, there are usually a small operations that need to be done with high privilege levels, so those components can have a small amount of code, can be audited more easily, and are likely to have fewer potential vulnerabilities.

Privilege separation can be achieved by starting distinct processes, each running under a distinct user ID, that communicate with each other over mechanisms such as named pipes or local sockets. One straightforward way to achieve privilege separation is to run a single process and then split it into two components, one running at an elevated privilege level. Each process has assigned to it a real and an effective user ID. The operating system determines privileges based on the effective user ID. Most of the time, they are equivalent. However, if an executable file is tagged with a setuid permission bit, it runs with the file owner’s user ID. In this case, the effective user ID (euid) is that of the file owner while the user ID remains that of the user. The operating system remembers who the user is for a process even for setuid programs. POSIX systems provide a system call called seteuid that allows a process to change the effective user ID. Unprivileged processes may only set the effective user ID to the real user ID while privileged processes can set it to any user ID.

We can achieve privilege separation between two components by setting a program with setuid privileges to the highest privilege level needed. When the program starts, it:

  1. Creates a communication link to itself using, pipes, named pipes, local sockets, shared memory or whatever other mechanisms the operating system provides.

  2. Use the fork system call to create a child process. Fork simply replicates the process, so the parent and child are running the same program. The only different is the return value from fork (the child gets back 0 while the parent gets the child’s process ID).

  3. The child will lower its privilege to that of the user by setting the effective user ID to the real user ID: seteuid(getuid)

  4. The parent will now call a function that only handles the high-privilege needs of the application. The child handles everything else.

  5. The parent and child communicate via the inter-process link the established in step 1.

There are variations of this. If you don’t want the risk of having even unused code resident in the high-privilege portion of the service, the parent can simply call execve to run a totally separate program to handle high privilege tasks. Even more simply, if a process can do its privileged operations early on, such as opening a restricted file, in can then drop its privilege by changing to another user ID without having to create or communicate with another process.

Modeling protection: the access control matrix

Figure 2. Access control matrix
Figure 2. Access control matrix

The primary generic abstraction for representing access control is an access control matrix. In this structure, each row represents a “domain”, which is a subject or a group of subjects. Typically, these are users and groups. Each column represents an object. Typically these are files and devices. The intersection of each object and subject contains the access rights that user has on the object. For example, a user in domain D2 has read-execute access for file F0.

We can formalize the the changing state of the system as a sequence of commands. Each command contains an optional condition followed by one or more primitive operations that define operations on objects, subjects, and rights. Primitive operations include:

  • Create subject
  • Create object
  • Enter right r into the access control matrix a at a[s, o]
  • Delete right r from the access control matrix a at a[s, o]
  • Destroy a subject s
  • Destroy an object r

For example, a Linux process p that creates the file f with owner read-write permissions can have its access control operations represented as:

command createfile(euid(p), f)
create object f
enter "own" into a[euid(p), f]
enter "read" into a[euid(p), f]
enter "write" into a[euid(p), f]
Figure 3. Domain transfers
Figure 3. Domain transfers

POSIX systems limit us to a setuid mechanism via files but otherwise only the root user can change the user ID. In the general case, however, the right to switch from one domain to another domain can be a configurable policy. We simply need to extend our list of object to include domains (subjects) as well. The switch attribute at the intersection of two domains specifies that the domain in the Domains row has the ability to switch to the domain in the column. Figure 3 shows that domain D0 has the right to switch to D1. Once switched, however, domain D1 does not have the right to switch to D0.

Owner attributes may be assigned to the matrix to specify that a specific domain is the owner of a specific object. Ownership gives the owner the right to add and remove any right for that object; they allow control of a rights in a column. For example, if D0 is the owner of F0, it can add write access for F0 to domain D2

Figure 4. Row-based control
Figure 4. Row-based control

We can also enable controls across a row. If the access right a[i, j] contains a control right (Figure 4) then a process executing in domain i can change access rights of any objects for domain j.

Note that these features are generally not available in any one system but are, in theory, possible and the access control matrix provides a framework to express and visualize these operations.

Access control lists

Figure 5. Access control list
Figure 5. Access control list

In practice, implementing an access control matrix in a system is not practical. Given that most systems will have tens or hundreds of thousands of files, shared servers may have hundreds of users, and many files are created and deleted, it is an unwieldy structure to manage. For this reason, systems most often use an access control list, which we just examined. It associates a column of the access control matrix with each object. When the operating system accesses the object, it also accesses the list of access permissions for that object. Adding new permissions is done on an object-by-object basis.

Capability lists

Figure 6. Capability list
Figure 6. Capability list

An access control list associates column with each object. That is, each object stores a list of access permissions for all the domains (subjects). Another way of breaking up the access control matrix is by rows. We can associate a row of the table with each domain (subject). This is called a capability list. A capability** is the set of operations that the subject is allowed to perform on a specific object. Each subject now has a complete list of capabilities.

Before the operating system performs a request on an object, it will check the subjects capability list and see if the requested access is allowed for that object. A process, of course, cannot freely modify its capability list unless it has a control attribute for all objects in the domain or is the owned of a specific object.

Capability lists have the advantage that, because they are associated with a subject, the system does not have to read any additional data to check access rights each time an object is accessed, as it has to do to read an access control list. It is also very easy to delegate rights from one user to another user: simply copy the capability list. If a user must be deleted, it is also easy to handle that: simply delete the capability associated with that user; there is no need to go through the access control list of every file in the file system.

In practical use, the disadvantages of capability lists outweigh its advantages. It is incredibly difficult to change a file’s access permissions on a global level: you have to go through ever user’s capability list and modify it. There is no easy way to find all users that have access to a resource short of checking every user’s capability list. Capability lists have generally failed to attain mainstream use. They were deployed in IBM’s AS/400 system and in a research system from Cambridge University called CAP. Beyond that, access control lists (either full ones or the truncated Unix versions) won out.

However, the idea of a capability list is useful with networked services. You often connect to servers that do not know you and where you do not have an account. In such cases, authorization and single sign-on services such as OAuth and Kerberos can provide an unmodifiable message stating who a user is and what operations they are allowed to perform.

Microsoft incorporated a form of capabilities into Windows (since Windows 2000). The primary access control mechanism on Windows is Access Control Lists (referred to as Discretionary Access Control Lists, or DACL). However, systems need to be able to support centralized account management and and authentication via Active Directory servers. Authentication is a sign-in process that returns a Security Identifier (SID) for a user as well as a list of SIDs for the groups of which the User is a.member. This information is then used to create a single access token, Which is attached to every thread and process that executes on the user’s behalf. Whenever the thread needs to access an object or perform any system task That requires user rights, the operating system checks that access token.

Capabilities did find their way into a mainstream operating system: Microsoft added support for capabilities since Windows 2000. The access control mechanism dominates but capabilities can override or complement access control lists. One can create profiles that permit whitelisting or blacklisting users. Access rights are defined for groups (called Group Policy) and are associated with either a single server or can span domains or organizations.

Mandatory Access Control (MAC)

With access control lists, subjects are in control: they set access permissions to their objects This is called Discretionary Access Control, or DAC. A user can set access rights to give access to a file to any other subject. For instance a user may run the command

chmod o+rw secret.docx

and give read-write access for the file to everyone. Access control lists were designed to work this way. However, this model does not work in environments where management needs to enforce its policies and define access permissions. Also, when access rights are associated with objects, it becomes difficult to turn off access for a specific subject except by locking the the user (e.g., disabling their account). The alternative is to go though each object in the system, check if the user is in the ACL for that object, and remove the user from the ACL.

Mandatory Access Control (MAC) is a form of access control where policy is centrally enforced and users cannot override the policy. With MAC, administrators are in charge of access permissions.

MLS: Multilevel Security Systems

Figure 7. Bell-Lapadula MLS access model
Figure 7. Bell-Lapadula MLS access model

Multilevel security systems were designed to handle multiple levels of classified data in one system. The Bell-LaPadula model is the best known of these systems. It was originally designed for the U.S. Navy to enable users with different classification levels to use a single, shared computer system. The military uses four classification levels: unclassified, confidential, secret, and top secret. You do not have access to information above your clearance and you cannot create information below your clearance.

With the Bell-LaPadula model, every object is assigned one of those four sensitivity levels. Every user is also associated with a clearance at one of those levels. If you have confidential clearance, you can read confidential and unclassified data but you cannot read anything of a higher clearance level: secret and top secret. You can create data at confidential, secret, and top-secret levels (yes, you can create files that you will not be allowed to read) but you cannot create unclassified data. The motivation for this model is confidentiality – the prevention of information leakage. If a piece of software is hacked or a user account is compromised, it will not be able to leak data from higher classification levels.

Bell-LaPadula is summarized as _no read up; no write down. You cannot read from a higher clearance level or write to a lower clearance level. More formally, the Bell-LaPadula model has three properties:

1. The Simple Security Property
A subject cannot read from a higher security level (no read up, NRU)
2. The *-Property (Star Property)
A subject cannot write to a lower security level (no write down, NWD)
3. The Discretionary Security Property
Discretionary access control (e.g., access control lists) can be used with the model after mandatory access controls are enforced. This means that you can, for example, block read-write access to your files and users who would have clearance to access those files will be disallowed access.

The ability to write data to higher classification levels brings up the possibility that someone can overwrite a file at a higher classification level. Overwriting the contents of the file would be an attack on availability. To avoid this, systems usually allow overwriting a file only if the process’ (subject’s) and file’s security labels match exactly and discretionary access controls permit the write. For example, a user with confidential clearance can only overwrite a file at the confidential level.

The Bell-LaPadula model has a tranquility principle that states that security labels never change during operation. This means objects and subjects always remain at the same classification levels. In practice, systems implement a weak tranquility principle, where security labels may change but only in a way that does not violate security policy. This is done to implement the principle of least privilege. If a user has top secret clearance, a program will run at the lowest clearance level and get upgraded only when, and if, it needs to access data at a higher classification level.

The Bell-LaPadula model was designed to fit the way the government treats classified data. However, in practice, it became a complicated model to implement and enforce. For instance, the model does not provide a way to declassify files (lower their clearance level). This can only be done via a special “trusted subject”, who has such authority. The model is also difficult to use in that it makes collaborative work among people of different classification levels essentially impossible. Finally, the use of shared servers, databases, and networking makes the model challenging. Can a database store data of varying security levels? If so, that is outside of the visibility of the operating system and has to be implemented within the database software. How about email? A user should only be able to send messages to users at the same classification level or higher. But users at a higher classification level would not be permitted to respond. The very concept of classification levels works for the military but does not map well to civilian business practices.

Biba Integrity Model

The Bell-LaPadula model was designed strictly to address confidentiality. The Biba model is a similar multilevel security model that is designed to address data integrity. With confidentiality, we were primarily interested with who could read the data and ensuring that nobody at a lower classification level was able to access the data. With integrity, we are primarily concerned with imposing constraints on who can write data and ensuring that a lower-integrity subject cannot write or modify higher-integrity data.

The properties of the Biba model are:

1. The Simple Integrity Property
A subject cannot read an object from a lower integrity level. This ensures that subjects will not be corrupted with information from objects at a lower integrity level. For example, a process will not read a system configuration file created by a lower-integrity-level process.
2. The *-property (Star Property)
A subject cannot write to an object of a higher integrity level. This means that lower-integrity subjects will not corrupt objects at a higher integrity level. For example, A web browser may not write a system configuration file.

The Biba model is essentially the opposite of the Bell-LaPadula model. The Bell-LaPadula model was summarized as no read up, no write down. The Biba model is no read down, no write up.

Microsoft implemented support for the Biba model in Windows with their Mandatory Integrity Control. File objects are marked with an integrity level:

  • Critical files: System
  • Regular users and objects: Medium
  • Elevated users: High
  • Internet Explorer: Low

A new process gets the minimum of the user’s integrity level and the file’s integrity level. The default policy is the Biba model’s NoWrite_Up.

The goal of this policy is to limit potential damage from malware: anything downloaded with Internet Explorer would be able to read files but would not be able to write them since IE runs at the lowest integrity level and all objects it creates will be at that level … even if the user’s integrity level was a higher value.

As with the Bell-LaPadula model, in practice, Biba also rarely fits the way things work in the real world. Even with Microsoft’s IE, there were far too many times when downloaded content needed to be brought to a higher integrity level (otherwise it was mostly useless). Trusted subjects could overwrite the security model and do this. Since it happened so often, users just got used to (although annoyed by) pop-up dialog boxes asking for permission. Microsoft dropped the NoReadDown restriction to make the system more usable but, in doing so, did not end up protecting the system from malicious content.

Type Enforcement (TE) Model

The Type Enforcement (TE) model is a mandatory access control system that simply implements an access control matrix that gives mandatory access control priority over existing discretionary access control mechanisms..

The model defines domains and types. Subjects (users and processes) are assigned to domains. Multiple users may be part of the same domain. Objects (files, sockets, devices) are assigned to types. Multiple files can be assigned the same type.

Type Enforcement is enabled by creating a matrix that defines permitted domain-domain and domain-type interactions. Basically, it allows an administrator to specify that a certain class of users has specific access to a class of files. If the TE model denies access, discretionary access control rules do not matter; the user may not access the file. If the TE model permits access, the system then checks the access control list for the object to see whether access to the file was granted by the user.

SE Linux, a Security Enhanced version of the Linux kernel, provides support for an extended Type Enforcement model. Domains and types share the same implementation: they are labels that are associated with a user or a file, enabling collections of users or collections of objects. Every object is labeled with a type. All objects are an instance of a particular class that has a set of operations (e.g., data type). An operation set is a subset of all the allowable operations in that class.

A permission associates a type, class, and an operation set. Hence, a set of objects can be assigned a specific set of access rights. Subjects (also types) are associated with permissions via allow statements. In essence, SELinux creates an access matrix between subject-object pairs and specifies their allowable operations.

Role-Based Access Control (RBAC) Model

The Role-Based Access Control model was created with business processes in mind. It is designed to be more general than multilevel models such as Bell-LaPadula (which didn’t map well onto civilian life) and is designed to enforce both MAC and DAC properties.

Access decisions are not based on user IDs but are based on roles. Administrators define roles for various job functions. Each role has associated with it permissions to perform certain operations. Users are then assigned to one or more roles.

A role can be thought of as a set of transactions or operations that a user or set of users can perform within the context of an organization. Note that roles relate to job functions, not specific access permissions. For instance “update customer information” is a role while “write to the database” is not a role. RBAC enables fine-grained access since roles can be defined in application-specific ways: transfer funds instead of read/write accounts.

Unlike access control lists, RBAC assigns permissions to roles rather than users. This creates an extra level of indirection between the subject and the object. The mapping between users and roles can change dynamically. For example, if a substitute worker comes in or a new employee joins a group, she can be added to the appropriate role (or roles) and have access rights necessary to perform her job. Similarly, the mapping between roles and objects can also change dynamically. If developers for a certain project no longer needs access to a specific source code repository, that access can be removed from the role and affect all developers on the project. Objects no longer need to have access rules that identify individual subjects.

RBAC requires the following conditions:

  1. Role assignment. A subject can execute an operation only if the subject has been assigned a role.

  2. Role authorization. A subject’s active role must be authorized for that subject. This ensures that users can only take on roles for which they have been authorized.

  3. Transaction authorization. A subject can execute a transaction only if the transaction is authorized through the subject’s role membership.

A role is conceptually almost similar to a group. However a group just a collection of users whereas a role is a collection of permissions.

In SELinux, RBAC is built on top of its Type Enforcement (TE) model. Users are mapped to roles at login time. Roles are authorized for domains and domains are given permissions to types.

RBAC is the dominant MAC model in larger corporations. It makes it easy to manage the movement and reassignment of employees. However, it may require application awareness for proper implementation. For example, databases may need to restrict specific operations based on roles:

  • Role A: cannot add or delete users to/from the table
  • Role B: can delete users but cannot change the salary of a user
  • Role C: can change the salary of a user but not add or delete users

Multilateral security

The Bell-LaPadula and Biba models are classic cases of multilevel security. Subjects and objects are assigned classification labels (or integrity labels in the case of Biba) and rules control what you can read or write.

The Bell-LaPadula model, however, doesn’t exactly implement how the military deals with classified data. In addition to the four classification tiers (unclassified, confidential, secret, top secret), each security level can also be divided into compartments. This is usually done at the top secret level but can be done at any level. In the government, this is referred to as TS/SCI – Top Secret / Special Compartmentalized Intelligence. Even if you have top secret clearance, you must be explicitly granted access to each compartment and are not allowed access to data in other compartments. This is a formalized implementation of the “need to know” principle.

Objects in a multilateral security model are not only identified by their clearance level but are further tagged with zero or more security labels (compartments). If you do not have clearance for a specific label then you cannot access the data regardless of your clearance level. For example, {Top Secret, UFO} data cannot be read by someone with only {Top Secret} clearance. Someone with {Secret, UFO} clearance also cannot read the data: they have the right label (compartment) but a lower clearance level. Conversely, someone with {Top Secret} clearance cannot read {Secret, UFO} data. Even though they possess a higher clearance level, they lack the compartmental clearance: the UFO label.

Lattice-based Access Control

Figure 8. Lattice model
Figure 8. Lattice model

Lattice-based Access Control (LBAC) is a way of representing access rights in a multilevel, multilateral security environment. Each node is a set containing a security classification level and zero or more compartment labels (see Figure 8). A directed edge between a pair of nodes identifies read access. For example:

  • Someone with {Top Secret, UFO, Elvis} labels can read {Top Secret, UFO} objects.
  • Someone with {Top Secret, UFO} labels cannot read {Top Secret, Elvis} objects.
  • Someone with {Top Secret, UFO} labels can read {Top Secret} objects.

The relations are transitive: because {Top Secret, UFO} can read {Top Secret} and {Top Secret} can read {Secret}, that means {Top Secret, UFO} can read {Secret}.

A problem with compartmentalization is that it creates more data isolation. Data from two compartments, for example {Top Secret, Elvis} and {Top Secret, UFO} creates a third compartment, {Top Secret, Elvis, UFO}. Real organizations often have thousands of compartment labels, which can create a combinatoric explosion of distinct compartments and does not help with data sharing when it might be useful. One option to limit the compartmentalization somewhat is to change the rules and allow a subject at a higher classification read access to all lower levels. Thus, someone with {Top Secret} clearance would be able to read {Secret, UFO} and {Secret, Elvis} data even without having UFO or Elvis Special Compartmentalized Intelligence.

Chinese Wall model

A Chinese Wall is a set of rules that are designed to prevent conflicts of interest. It is commonly used in the financial industry but is also common in law firms and advertising agencies. For example, banks have corporate advisory groups that work with companies on mergers and acquisitions, debt financing, and IPOs. Banks also have a brokerage group that handles investments. It would be a conflict of interest – and illegal – for an advisory group member to tell someone in the brokerage group about a pending acquisition that will likely affect the stock price of a company. Hence, a Chinese wall is erected between these divisions.

The Chinese Wall model can also implement a separation of duty. To avoid fraud, for example, a person may be permitted to perform just one of several necessary transactions but is explicitly disallowed from performing more than one.

There are three layers of abstraction in implementing a Chinese wall:

  1. Objects. These are files that contain resources about one company.

  2. Company groups. These are the set of files that belong to one company.

  3. Conflict classes. These identify groups of competing company groups. For example {Coca-Cola, Pepsi} could be conflict classes. So can { American Airlines, United, Delta, Alaska Air } or { AT&T, Verizon, T-Mobile, Sprint }.

The basic rule of the Chinese Wall is that a subject can access objects from a company as long as it never accessed objects from competing companies.

More formally, the Chinese Wall model has two properties:

1. The Simple Security Property
A subject s can be granted access to an object o only if the object is in the same company group as objects already accessed by s or o belongs to a different conflict class
2. The *-Property (Star Property)
Write access is allowed only if access is permitted by the simple security property and no object can be read which is in a different company dataset than the one for which write access is requested and contains unsanitized information.

The term “unsanitized information” refers to data where the company’s identity is known. In some cases, data may be “sanitized”, meaning that the company’s identity is disguised to allow generation of bulk analytics or comparisons (e.g., average profit margins among competitors).


Even if access control mechanisms work perfectly, policies may fail. With DAC, you are trusting the users to set everything up correctly and likely trusting a system administrator to set up correct defaults (e.g., umask) and access controls for system services. With MAC models, user or role assignment may be done incorrectly. For multilevel models, collaboration needs must be considered. Models such as Bell-LaPadula and Bida require overrides to be useable.

There is still the concern of attacks on the operating system and system configuration files.This is an attack on the Trusted Computing Base and these attacks may change the definition of roles or the mapping of users to roles, subverting security.

Users are also a perennial concern. People are subject to social engineering attacks or may download dangerous software inadvertently. Most malware is installed willingly. Users thus are all too happy to give it privileges of – at least – those of normal applications, if not higher (e.g., you think you’re downloading a virus checker). As far as the operating system is concerned, it continues to do the right thing and is enforcing the defined policy.


This is an updated version of a document created on February 3, 2017.

  1. On Unix-derived systems (Linux, BSD, macOS, Android), most devices present themselves as files.  ↩

  2. Internet of Things, a trendy way of referring to network-connected computers that we don’t think of as computers, such as TV sets, DVRs, routers, security cameras, cars, and controllable lights.  ↩

  3. Most 8- and 16-bit processors designed for embedded systems do not support an MMU. Some simpler 32-bit processors designed for embedded systems, Such as the Arm Cortex-M0, also do not support an MMU.  ↩

  4. Examples of trap instructions are the Intel INT (“interrupt”) instruction or the ARM SWI (“software interrupt”) instruction.  ↩

  5. The original design of Multics called for 64 rings.  ↩

  6. hard links (the ln command or the link system call) allow you to create multiple file names that all refer to the same file.  ↩

  7. It’s more complex on macOS. They still have the /etc/passwd file, probably for some legacy programs that expect to read it but really store user identities under the /private/var/db/dslocal/nodes/Default/users directory, with a separate XML plist file for each user. It’s messy.  ↩

  8. The first process in Unix was init, which in turn spawns a set of programs that start other processes. In Linux, the first non-kernel process is init_task, which is compiled into the kernel and has a process ID of 0. It spawns the init process, which has a process ID of 1 and is responsible for starting all the other processes necessary to get the system running.  ↩

  9. Ports in the range 0..1023 are known as well-known ports and require root privileges to access.  ↩

Last modified November 25, 2020.
recycled pixels