pk.org: Computer Security/Lecture Notes

Part 1 - Protection and the Access Matrix

The operating system as gatekeeper, subjects and objects, access control lists, and capabilities

Paul Krzyzanowski – 2026-10-08

On March 22 and 23, 2019, a former Amazon Web Services engineer sent a series of requests to a web application firewall that Capital One, one of the largest credit card issuers in the United States, ran on a virtual machine in Amazon’s cloud. The firewall was misconfigured so that it could be made to fetch a URL supplied in a request it was inspecting. The URL she supplied was the virtual machine’s metadata service, an internal address that hands out the temporary credentials of the role the machine runs as. The firewall returned them to her. With those credentials, and from her own computers, she listed the bank’s storage buckets and copied their contents, nearly 30 GB of data.

The data covered about 106 million credit card applicants in the United States and Canada, including about 140,000 Social Security numbers and 80,000 bank account numbers. Capital One learned of the breach from an outside tip on July 17, 2019, and announced it on July 29. In August 2020, the Office of the Comptroller of the Currency, the regulator of national banks, fined the company $80 million for the failures that enabled this breach. The attacker was convicted of wire fraud and computer intrusion in June 2022.

The firewall did not need to read customer records. Its job was to inspect web traffic. Every access control system decides whether a subject may perform an operation on an object, and for that firewall the policy on record allowed it. The attacker never defeated the check. She used the firewall to obtain a credential that the check already trusted, and then presented that credential herself.

Access control is the part of security that decides which subjects may do what to which objects, and then enforces the decision. It’s a combination of policies and mechanisms.

The sections that follow cover where that enforcement sits in a computer, the model that describes what should be allowed, and the two ways that real systems store that model. After that comes discretionary access control, where the owner of a file decides who may use it, with the Unix permission system as the example, and then the mandatory models, where owners cannot overrule system-defined policies.

The Operating System as Gatekeeper

An operating system’s job is to provide processes with controlled access to the resources of the machine. Four resources are shared by every process, and for each of them there is a reason a program cannot take what it wants without asking:

Protection refers to the mechanisms that enforce controlled access to these resources. A security policy defines what the controlled access should be. The distinction between a policy and the mechanism that enforces it is the organizing idea of access control. The access control models described later are ways of writing policies. The hardware and kernel features in this section are the mechanisms that make any of those policies enforceable.

Three things have to be true for protection to work at all:

  1. The operating system must protect itself from the programs it runs, so that an application cannot overwrite kernel code or data.

  2. It must protect programs from each other, so that one process cannot read another’s memory (a confidentiality failure) or change it (an integrity failure).

  3. It must stay in control, so that a process cannot hold the processor forever or stop the kernel from running.

MS-DOS, early versions of Windows, and the classic Mac OS through Mac OS 9 did none of these: a single misbehaving application could crash the machine. The kernel and the hardware that enforce these three properties are the core of the trusted computing base, the set of components whose failure means the security of the whole system fails.

Regaining the Processor

A processor executes whatever instruction comes next. If a program contains an infinite loop, nothing in the program will ever hand control back to the operating system. The operating system relies on a programmable hardware timer that generates an interrupt at a fixed interval, traditionally about 100 times per second, although current systems vary the rate to save power. When the interrupt arrives, the processor saves where it was and jumps to a kernel routine. Applications cannot turn the timer off, because the instructions that control it are privileged.

The timer interrupt is what makes preemptive multitasking possible. The kernel’s scheduler runs at each interrupt and decides whether the current process has had its share of the processor. A process that has been running too long is suspended and another is resumed. The switching is fast enough that each process appears to have the processor to itself, which is the illusion that time-sharing systems have provided since the 1960s. The property this protects is availability. A process should not be able to starve the others or keep the kernel from running.

Early versions of Microsoft Windows and the classic Mac OS used cooperative multitasking instead. Programs were expected to yield the processor voluntarily, and a program that did not yield froze the computer. The hardware timer removes that dependence on good behavior.

Isolating Memory

Nearly every processor built for a general-purpose operating system includes a memory management unit, or MMU. The MMU divides memory into fixed-size pages, typically 4 KB, and translates every address a program uses, its virtual address, into a physical address in the memory chips. The translation goes through a page table, and each process has its own. An entry in the page table says where a page lives in physical memory and how the process may access it: read, write, or execute.

This translation allows the operating system to isolate the memory of one process from other processes. A process can only refer to memory that its page table maps. If two processes’ page tables point to disjoint physical pages, neither can touch the other’s memory, regardless of the addresses its code uses.

The MMU and the operating system together give each process the illusion that it has all of memory to itself: its own addresses start at zero and run to the top of its address space, and the memory of every other process is invisible to it. In the traditional design, the kernel’s own pages appear in every process’s page table, marked so that only the kernel may touch them; the processor mode that makes that distinction is described next. When the kernel switches from one process to another, it loads that process’s page table into the MMU, which on Intel processors means writing a new value to the cr3 register.

An access that the page table does not allow causes a page fault, which transfers control to the kernel. Most page faults are expected because the process is accessing legitimate regions of memory that are not yet loaded into physical memory. The kernel loads the page from disk or makes a private copy of a shared page, updates the page table, and restarts the instruction. A fault on a page outside the process’s allowed regions, or a write to a page the process may only read, is the other case, and the kernel then terminates the process or delivers a signal to it. In every case, the hardware refuses the access before it happens and leaves the decision to the kernel.

Small embedded processors, such as the Arm Cortex-M0 found in many microcontrollers, do not have an MMU. They rely on simpler protection units or on none at all, which is one reason that security on small devices is harder.

User Mode and Kernel Mode

The timer, the MMU, and the devices are all controlled by instructions that must not be available to ordinary programs. A program that could reprogram the MMU could map any memory it wanted. Processors solve this with privilege levels, also known as modes. At a minimum there are two:

A process in user mode that attempts a privileged instruction or a forbidden memory access does not get an error code. It gets a trap, which transfers control to the kernel at an address the kernel chose when the system booted. There are three ways for a user process to enter kernel mode, and they all transfer control to the operating system:

  1. A system call. The program executes a trap instruction after placing the number of the service it wants and its arguments in registers. The processor switches to kernel mode and jumps to the system call handler. This is the only way for a program to request a file, a device, the network, or any other operating system service.

  2. A fault. The program did something it was not allowed to do, such as dividing by zero or accessing a page it is not allowed to use. The processor raises an exception, which transfers control to a preset address in the kernel. The kernel decides how to handle it. A page fault can be a legitimate request, in which case the operating system loads the page into memory and allows the process to continue running.

  3. A hardware interrupt. A device or the timer needs attention. The current process is paused, the kernel handles the interrupt, and the process resumes later. Examples of this are timer interrupts (to force the kernel scheduler to run), disk interrupts to inform the kernel that some disk activity has completed, or a network interrupt to indicate that data has arrived on a network interface.

When the kernel finishes, it restores the saved state and returns to user mode. The application’s own instructions never ran in kernel mode; the kernel’s code ran in the process’s context, on the process’s behalf.

Some processors offer more than two levels. Multics, the 1960s time-sharing system from MIT, Bell Labs, and General Electric whose ideas led to Unix, divided its software into eight protection rings, with the most trusted code in ring 0 and ordinary programs in the outer rings. Intel’s 80286 processor, introduced in 1982, provided four rings. Intel and AMD processors still support all four. Every mainstream operating system that runs on them uses two: ring 0 for the kernel and ring 3 for applications. The middle rings went unused because other processor families did not have them, and an operating system that used them would not port.

Virtualization added a level below the kernel. A hypervisor runs several operating systems on one machine, and each guest kernel runs in ring 0 in guest mode. The hypervisor runs in a mode more privileged than that, which Intel calls VMX root mode and which is informally known as “ring -1”. The name is shorthand: the guest keeps its rings, and the hypervisor decides which guest operations it intercepts. Arm and RISC-V processors have an equivalent level. The same relationship holds at every layer: each level of software is constrained by the one below it, and the hardware is what enforces the constraint.

The Reference Monitor

In 1972, a study for the U.S. Air Force led by James P. Anderson asked what a computer would need in order to be trusted with classified information. Its answer was a component it called the reference monitor: the part of the system that checks every attempt by a subject to reach an object against the policy. The report gave three requirements for it:

In an operating system, the kernel’s access checks are the reference monitor. The hardware supports the first two requirements: every access to memory goes through the MMU, every other access goes through a system call or a mapping the kernel approved, and user mode cannot modify the kernel. The hardware cannot do more than that. It keeps applications out of the kernel, but a bug in the kernel or in a driver that runs inside it can still violate the policy. The third requirement is harder to meet. A current Linux kernel is tens of millions of lines, and the CrowdStrike failure in July 2024 showed the risk of placing a vendor’s code into that trusted layer.

Windows calls its implementation the Security Reference Monitor, and we will use the term again for SELinux, which adds a second reference monitor inside the Linux kernel.

Many access decisions are not made by the kernel at all. A database server decides which tables a user may read. A web application decides which accounts a logged-in user may see. In each case the program is its own reference monitor, and the kernel protects the program’s memory and files without knowing anything about its tables or accounts. Capital One’s firewall and the cloud storage service were both of this kind. The access control models described here apply to them as much as to the kernel, but the enforcement is only as good as the program.

Subjects, Objects, and Rights

Access control needs a vocabulary for the things it decides about. A subject is an active entity that requests access, which in an operating system is almost always a process. A principal is the identity on whose behalf a subject acts, usually a user account, and every process has a principal it is acting for. An object is a passive resource that a subject wants to use: a file, a directory, a device, a socket, a region of memory, or another process. A right is an operation that a subject may perform on an object, such as read, write, execute, or delete.

When Alice’s editor opens a file, Alice is the principal, the editor process is the subject, and the file is the object. Principal, subject, and user are often mixed in ordinary usage, and these notes mix them too. A user is the casual term for a principal, and a subject acts on behalf of a principal, so it is common to say that a user accesses a resource when what happens is that a process acting for that user accesses it.

Before any of this can work, the system has to know who the principal is. Authorization is the last of three steps, and the first two were covered with authentication: identification is a claim of identity, authentication verifies the claim, and authorization decides what the authenticated principal may do. Access control is authorization. Everything that follows assumes that the user behind a process has already been authenticated, and the question is what that process may do.

The Access Matrix

In 1971, Butler Lampson, then at the Xerox Palo Alto Research Center, proposed a single structure that describes every permission in a system. The access matrix has one row per subject, one column per object, and in each cell the set of rights that the subject has over the object. Lampson used the term protection domain for a row, because several processes may share the same set of rights, and the rights belong to the domain rather than to any one process.

File A File B Printer
Alice read, write read print
Bob read
Process X write

The matrix says that Alice may read and write File A, read File B, and use the printer, that Bob may only read File A, and that Process X may only write File B. An empty cell means that the subject may not access the object at all. Every access decision the system makes is a lookup: find the row for the subject, the column for the object, and check whether the requested right is in the cell.

Some rights are not about the object’s contents. They are about the matrix itself, and real systems need them:

The matrix is a model. It says what the system should allow without saying how the system stores or checks it, and that separation is what makes it useful. A Unix file permission, a Windows access control list, and a cloud identity policy are all ways of storing part of an access matrix, and seeing what each one leaves out and how each manages storing the protection rules is a good way to understand it.

Why the Matrix Is Never Stored as a Matrix

A system with 1,000 users and a million files would need a billion cells. Most users have rights over very few files, so the vast majority of cells would either be empty or hold the default rights that apply to any user, and the table would take an enormous amount of space to record very little. Keeping the table current would be the larger problem, since files are created and deleted and users come and go constantly.

Real systems therefore store the matrix in a compressed form. Cells that would be empty are left out, and many of the cells that remain hold a default rather than an individual entry, such as the Unix permission bits that apply to everyone other than a file’s owner and group. The compression is done in one of two ways. Either each column is stored with its object, or each row is stored with its subject.

Access Control Lists

An access control list, or ACL, stores a column of the matrix with the object it describes. Each file, device, or other resource has a list of entries, and each entry names a subject and the rights that subject has. When a process opens a file, the kernel looks up the file’s list, finds the entry for the process’s principal, and checks whether the requested right is there.

ACLs make some queries easy and others hard. “Who can read this file?” is a single search through the list. Revoking a user’s access to a file is one edit to one list. On the other hand, “what can this user access?” requires searching every object’s list, and removing every entry for a user means editing every list that names that user. Checking at every access also costs a search through the list, which is why Unix compresses its list to three fixed entries: owner, group, and everyone else. Linux and macOS additionally support full ACLs, which keep the list form but let it hold any number of entries, and Windows has used full ACLs since Windows NT.

Capability Lists

A capability list stores a row of the matrix with the subject. Each process holds a list of capabilities, and each capability is a token that names an object and a set of rights over it. The idea comes from a 1966 paper by Jack Dennis and Earl Van Horn at MIT. The defining property of a capability is that it cannot be forged. A process cannot create a capability for a file it was never given; it can only receive one from the kernel or from another process that already holds it.

The access check with capabilities is the presentation of the token. A process that holds a capability for a file with the read right does not need to have its access permissions looked up in any list. It presents the capability. The kernel then verifies that the token is genuine and that the right is in it. Delegation is handing a copy of the token to another process. Checking is fast. The hard operation is revocation. To take away a right, the system has to find every copy of the capability that was ever handed out, or make them all invalid at once by some indirection.

The two representations answer opposite questions. An ACL answers “who can access this object?” A capability list answers “what can this subject access?” Both store the same matrix, and the choice between them is about which operations should be cheap.

Capabilities Already in Use

Capabilities sound exotic, but every system in daily use has them, just not always directly. An open file descriptor on Unix is one. The permission check (via an Access Control List) happens once, when open() is called. After that, the descriptor is a token that the process presents on every read() and write(), and the kernel does not recheck the file’s permissions. A descriptor can be inherited by a child process, and it can be sent over a Unix domain socket to a process that could not have opened the file itself. That process can then read the file, because the capability, not the identity of the holder, is what the kernel checks.

A Kerberos ticket is capability-like. The authentication server checked the user once, and the user then obtains a ticket for each service and presents it to that service, along with proof that the user holds the key sealed inside it. The ticket differs from a true capability in that it names a principal rather than an object and a set of rights, and the service still decides what that principal may do. Temporary cloud credentials are the same kind of thing. The Capital One attacker never had a password, only the temporary credentials that the firewall’s role had been issued, and the storage service checked that role’s permissions on every request.

Entire operating systems have been built on the idea of capabilities, from KeyKOS in the 1980s to the seL4 microkernel and Google’s Fuchsia today, where every resource a process can reach is represented by a capability it holds and nothing else.

The Confused Deputy

Capability systems were motivated in part by a problem that Norm Hardy, one of the designers of KeyKOS, described in 1988. A confused deputy is a program that holds authority of its own and can be tricked into using that authority on behalf of a caller who does not have it. Hardy’s example was a compiler that was allowed to write a billing file. A user who asked it to write its output to the billing file’s name got the compiler to overwrite it, because the compiler, not the user, was the subject the access check saw.

The firewall at Capital One was a confused deputy for one request. It had permissions to reach the metadata service that hands out credentials. The attacker could not, but she had the firewall fetch them for her. Everything after that she did with the credentials directly.

A Unix program that runs with more privilege than the user who invoked it is a deputy as well, and it becomes a confused one when it fails to check what its caller is entitled to. The deputy’s rights are checked, but the caller’s are not. Capabilities make the problem avoidable by letting the caller hand over the authority for each object it wants used, so that a program can act only on objects its caller could have reached. They do not avoid it automatically: the program still has to keep its own authority separate from what its caller supplied. Systems built on ACLs instead rely on each privileged program to check on its own whether its caller should be allowed what it is requesting.

Who Decides: Discretionary and Mandatory Control

The access matrix says what is allowed. It does not say who gets to change it, and that question divides access control into two families.

In discretionary access control, or DAC, the owner of an object decides who may access it. The user who creates a file holds the owner right on its column and can grant or revoke rights to other users at will. The system enforces whatever the owner decided, but it does not impose a policy of its own. Unix file permissions, Windows file permissions, and most of what any user does with files on most systems are discretionary.

In mandatory access control, or MAC, the system enforces access rules that the organization defines rather than the owner. Every subject and object has a label assigned by an administrator or by policy. The rules that compare labels are fixed, and the owner of an object cannot overrule them. A user cleared to read a classified document cannot make it readable by another user who is not, however much they want to. Mandatory models exist because discretionary control cannot express a rule that the owner is not allowed to break.

Nearly every system implements some degree of both, although discretionary control dominates in most of them. In the discretionary model that Unix introduced, just a few bits in a file’s inode hold that file’s column of the matrix. The same model creates a problem when a program needs more rights than the user who ran it, and that problem is where the discussion of Unix leads.


Next: Part 2: Discretionary Access Control in Unix


Lecture 6: Part 1 | Part 2 | Part 3
Lecture 6 Study Guide | List of terms