USENIX '04 Paper
[USENIX '04 Technical Program]
Modular Construction of DTE Policies
This paper describes a tool which composes a policy for a fine-grained mandatory access control system (DTE) from a set of mostly independent policy modules. For a large system with many services, a DTE policy becomes unwieldy. However, many system services and security extensions can be considered to be largely standalone. By providing for explicit grouping, namespaces, and globbing by namespaces, inter-module access rules can be made generic enough to permit modules to be mixed and matched as needed. As a result, it becomes easier to extend a policy, debug a policy, and to distribute meaningful policy modules with new software.
Domain and Type Enforcement (DTE)  is a fine-grained mandatory access control system. An implementation exists for Linux as a Loadable Security Module (LSM) . The DTE LSM reads the policy it enforces through a text file through sysfs. The policy language closely resembles TIS's original DTEL policy language, which was explicitly intended to be intuitive to read and write. We have previously presented tools to analyze and edit policies. We now present a tool to compose policies from policy modules, which are smaller, simpler policy excerpts. In practice, we find policy modules far simpler to work with than a single large policy.
We begin by describing DTE and DTE policies in more detail. Next we describe the syntax of a policy module. We describe methods of grouping types and domains, the priority assigned to access rules based upon source and target, and hooks for system interaction during policy compilation. Then we describe a ftpd protection policy previously presented, and show how the ftp-relevant portion of this policy becomes a module.
DTE specifies two types of labels, called types and domains. It assigns types to files, and domains to processes. File access is controlled from domains to types, and signal access is controlled between processes in different domains. A process may transition to a new domain only on a call to execve. A domain may only be entered through files labeled with types explicitly marked as entry types for that domain. These files are called entry points for that domain. Domains may only transition to certain other domains. There are two types of domain transitions. The first, called exec, is voluntary, while the second, called auto, is mandatory. When a process under some domain executes a file which is an entry point to another domain, to which the first domain has auto access, then the process will transition to the new domain. If the file was an entry point to another domain to which the first had exec access, then the process ordinarily does not switch domains. It may request a domain transition by performing
echo -n <new_domain> > /proc/<pid>/attr/execbefore executing the new file.
The DTE policy file specifies all types, all domains, the file system's type assignments, domain to type access, domain signal access, permitted domain transitions, and domain entry points. See  for more details about the policy file.
Policies for fine-grained MAC systems are mostly constructed as one unit and by hand. For instance, a massive effort is under way to create a complete, safe, SELinux policy for several distributions of Linux [13,14]. Tools exist  for analyzing DTE policies, and such work is also being done for policies of other fine-grained MAC systems [9,12,15]. Nevertheless, working with a large policy remains a painful experience. However, when working with large policies, patterns begin to emerge. Policies typically consist of several sets of domains and types. The entities within a set work together to achieve some goal, and the sets often interact very little. For instance, in the ftp policy presented in , the domain ftpd_d, and the types ftpd_t and ftpd_xt, work together to protect the system from an unsafe binary. By removing these entities, and all references to them, the remaining policy becomes simpler. We call this collection of domains, types, and all access rules pertaining to them, a module. The ftp module is shown in Figure 3, and will be described in Section 4.
Allowing policies to be composed from simple, meaningful, and coherent pieces will serve several purposes. First, creation of policies will become far more efficient. For instance, when adding a new domain to an existing policy, one might have to enter hundreds of type accesses in order to get it properly interacting with the current policy. In contrast, modules allow domains and types to be grouped at several levels, and access to be specified using any of these groups.
Second, adding a feature to a policy, such as a new method of controlling access to the shadow file, or protection from a critical binary in which an as-yet unsolved vulnerability has been found, will become a simpler task. The module can be written entirely from its own point of view. Furthermore, in researching the state of the current policy, in order to understand how to properly insert a new feature, one need only look at those modules which can affect the new functionality.
Third, modules may be helpful in simplifying the analysis, and proof of invariants, of policies. For instance, several modules may be trivially shown to be irrelevant to the ability of the inetd daemon, if remotely exploited, to erase the utmp log file.
Finally, because a module generally encodes domains, types, and access rules which work together toward some end, it is a natural way to express the security policy changes necessary for a new piece of software. Software companies and free software groups, therefore, could distribute policy modules along with software packages.
We now discuss the structure of a module file. The module syntax specification follows.
<module_file> ::= <module>+ <module> ::= Module <mod_name> [<domain_def> | <type_def>| <group_def>]+ end <domain_def> ::= domain <dom_name> <dom_line>+ end
A module file may contain more than one module. Each module may contain several domain, type, and group definitions, as well as the access rules pertaining to them.
<dom_line> ::= entries <type_name>+ | [absolute] signal [in|out] <gen_dom> <sig_num> | [absolute] domain [in|out] <gen_dom> [auto|exec|none] | [absolute] type <gen_type> <type_acc> | assert <policy_name> <data> | DEFAULT_DOMAIN
The domain definitions declare a unique name for the domain, a set of entry types, and a set of access rules pertaining to the new domain. Domain transition or signal access rules may be in, in which case they specify access from other domains to the new domain, or they may be out, defining access from the new domain to other domains. Since types are passive objects, which cannot themselves access other types or domains, the type access rules in a domain definition do not include the in or out keyword.
Exactly one domain definition applied to a policy must contain the keyword DEFAULT_DOMAIN. That domain will be assigned to the first process on the system.
<type_def> ::= type <type_name> <type_line>+ end <type_line> ::= <path_type> <path_name>+ | [absolute] access <gen_dom> <type_acc> | <default_type> | assert <policy_name> <data> <default_type> ::= DEFAULT_ETYPE | DEFAULT_UTYPE | DEFAULT_RTYPE
Type definitions declare a unique name for the type, a set of paths assignment rules, and a set of access rules. Clearly, the access rules are only incoming from domains. A type whose definition contains the keyword DEFAULT_RTYPE will be assigned to the root of the file system, and recursively to its descendants until another type assignment rule applies. Alternatively, one type may be labeled as DEFAULT_ETYPE, and another may be labeled as DEFAULT_UTYPE. The first will be assigned to the root of the file system, and the second will be assigned to its descendants until another type assignment rule applies.
Both type and domain definitions may contain assert statements. These are used for maintenance of policy constraints. They are stored with the type definition until module application, but their interpretation and enforcement is defined by the named policy consistency class, any number of which may be written by the policy authors to ensure the maintenance of any module properties. The last line of the ftpd_xt type definition in Figure 3 is an example of an assert statement, instructing a module loaded as blp to label this type as protected.
<group_def> ::= group domain <dom_name> import <dom_name>+ end <group_def> ::= group type <type_name> import <type_name>+ end <gen_dom> ::= all | none | <dom_name> <gen_type> ::= all | none | <type_name>
Grouping is accomplished on several levels. First, the keyword all refers to all domains or types which are currently known. Second, a group definition in a module may bind a name to a set of domains or types.
For instance, the following module segment defines a group of domains which may transition to user domains, and may require to files such as .bashrc and .xsession.
group login_domains_g import login_d su_d end
A separate x11 module might extend this group using
group login_domains_g extend import xdm_d endin order to borrow login_d's and su_d's rights to read user login files.
The following module segment defines a type which is actually called root_t.
type base.extraneous.root_t DEFAULT_RTYPE [...] endSince root_t is the type name which will be used in the final DTE policy, no names within the namespace may actually clash. Modules may refer to this type using any of the following names:
In addition, any type groups which have imported this type can also be used to refer to this type.
The name base.extraneous may be a real type, or it may simply be a namespace placeholder, depending on whether any module defines a type by that name. A namespace placeholder is the parent of a domain, type, or group, which is not itself defined to be a domain, type, or group. It can be referred to during namespace globbing, but will not appear in the final policy.
Namespace globbing works as follows. When a name ends in .+, it refers to all descendants under this name. When a name ends in .*, it refers to only the immediate children of this name. Therefore base.+ includes base.extraneous.root_t, but base.* does not. If base.extraneous were itself a type, then base.* would include this type, as would base.+.
Since domains and types can declare conflicting access rules, we must clearly define the priority of access rules. Much thought has been given to the current priorities, which have been somewhat modified following experience with an earlier module compiler prototype. The priority takes the form of an integer between 1 and 12. The priority assigned to access rules is shown in Figure 1.
Each type of access consists of three pieces of information. First, it can be in or out. This is relative to the type or domain in which it is defined. When a domain specifies a certain type access, this is a out access rule, as the access is outbound from the domain. If a type defines access from some domain, this is in, as the access is inbound from the domain to the type. The second piece of information relates to the precision of the rule target. When an access rule names a specific domain or type, this is single access. If the rule names a group, or a namespace expansion such as Services.*, this is group access. If the rule targets the keyword all, this is of course all access. Finally, the rule is either absolute or not. This depends only upon whether the access rule is preceded by the keyword all.
If two conflicting rules have been defined pertaining to the access permitted from a domain to another domain or type, then the rule with the highest priority will be applied. For instance, the base module's definition of type base_t specifies that all domains have absolute access rxld (read, execute, lookup, and descend) to base_t. This rule is absolute all in, and therefore has a priority of 8. Assume we write a new module, defining a domain intended to contain untrusted code. The domain definition might contain the statement:
absolute type all none
This rule is absolute all out, and therefore is priority 7. Since an absolute all in access rule has a higher priority than absolute all out, the new untrusted domain will receive rxld access to base_t, even though it asked for none. Had it in fact gotten none, then it would not be able to access any types at all, as it could not descend to them through the root of the file system. Similarly, if any types defined in the new module are intended to be accessed by the untrusted domain, then these types must specify incoming access from the untrusted domain as absolute, to ensure that that it will override the untrusted domain's outgoing type access definition. On the other hand, the base policy specifies a type bin_t, which includes a normal group in definition. As this is of a lower priority than absolute out, the access rule specified by the new module's untrusted domain is chosen, denying the untrusted domain all access to type bin_t. As we will see, this is a crucial element of the ftp module, preventing the ftp server from providing attackers with root shells, for instance.
Note that incoming access overrides outgoing access for the same target precision and absolute status. More specific rules override more general rules, unless the absolute keyword is present in one of the rules.
The usage of these keywords is intended to be intuitive. However, a switch to usage of simple numeric priority has not been ruled out. For instance, in place of
absolute domain in login_domains_grp auto
a module would specify
domain in login_domains_grp auto 60.
The disadvantages to this are that module authors might require a deeper understanding of how policy compilation is affected by the priorities, and would need to consider these effects explicitly for each access rule.
A set of modules may be applied simultaneously, and more than one set may be applied in series. For instance, we may begin by combining a set of base modules, then apply a set of service modules, and finally apply a module to ensure a particular security feature. We must therefore clearly define the behavior of group expansion across multiple module applications.
For named domain and type groups referenced in access rules, the group is
expanded at the time of module application.
In other words, for each member of the group, a new access rule is defined
with the same access details as the original rule. Each newly created
rule is associated with a group priority, to ensure proper resolution of
any future conflicts. If the group has not yet been defined, an error is
raised and compilation fails. For namespace globbing, that is,
domain some_domain type base.exec.+ rwx end
If the only children of base.exec defined thus far are the two types base.exec.sbin and base.exec.bin, then only these types are included in this rule. A later module may define type base.exec.javabin, but this type will not be added to the access rule.
The all target keyword is treated somewhat differently. An access rule directed at all will be expanded at the time of module application. Again the new access rules resulting from the expansion are stored with an all level for later conflict resolution. However, a generic form of the rule is also stored. All such generic rules are expanded each time a set of modules is applied. If the rule had not previously been applied, any policy consistency modules will be consulted at the new rule creation, just as with any other new access rule. For example, the base module defines default access rld to type base_t for all domains. This rule is expanded after each module application, so that all domains will be granted this access.
One of the goals listed in Section 2 for the use of policy modules is to facilitate distribution of policy modules with new software. It must therefore be possible to apply policy modules across a variety of systems. To accomplish this in any meaningful way will often require some bit of system interaction. For instance, a policy module distributed with xdm might require labeling each user's $HOME/.xsession as an entry type to the user domain. This requires system interaction to determine valid users on this system who actually have a $HOME/.xsession file.
The prototype module compiler provides system interaction through an exec keyword. This is augmented with looping support over variables which have been set using exec. Using these features, an excerpt of the xdm policy might look as follows:
1 define xsession_f exec /bin/ls \ 2 /home/*/.xsession 3 4 type xdm_out_fromuser_et 5 epath /etc/X11/xdm/Xsession 6 7 foreach file `xsession_f` 8 epath `file` 9 endforeach file 10 11 access user_d rwxlcd 12 access login_domains_grp r 13 end 14 15 domain user_d extend 16 entries xdm_out_fromuser_et 17 end
The first command, on lines 1 and 2, assigns to the variable xsession_f the result of executing the command /bin/ls /home/*/.xsession. This will contain a list of all user .xsession files, one per line. Lines 7 through 9 loop over each line returned by the ls command, each time adding a new epath line to the xdm_out_fromuser_et type definition, and replacing `file` with the next file. The result is a type to which the user domain may write, and which those domains which are members of the login_domains_grp group may read. The last three lines extend the user_d domain such that other domains may transition into it by executing the .xesssion files which were found. Of course, in many cases more complicated calculations than a directory listing will be required. The output from any script or program can be assigned to variables. However, the use of complicated external scripts might add an unwelcome element of unpredictability to the policy creation process. The policy consistency classes will offer some support to system administrators trying to keep this in check, and graphical analysis tools will remain available for analyzing the final policy.
An issue which may deserve further consideration is that of inheritance. It would seem to make sense to construct the type namespace such that certain properties, perhaps absolute access rules, are automatically inherited by the children of a type. On the other hand, this may simply needlessly complicate the process of policy creation, the simplification of which is the precise goal of the policy compiler. Currently, the notion of inheritance does not exist in the module compiler.
Ftp daemons provide a great deal of interaction, usually with completely unauthenticated, or anonymous, users. In order to permit user logins, however, some ftp daemons run as root. A programming error such as buffer overflow or string format vulnerability can therefore lead to the execution of arbitrary commands using superuser privileges by anyone on the internet.
Figure 2 demonstrates policy to protect a system from ftpd. While a DTE system could actually boot and run with this policy, it is a minimalist policy designed only to protect from ftpd. An actual useful policy would be much larger, but contain a nearly identical set of ftp protections. The policy provides protection from attackers by containing the ftp daemon to a domain, called ftpd_d, which has limited access rights. This domain is not allowed to transition into any other domains, so that any code executed (legitimately or not) by the ftp daemon will also be subject to the same access restrictions. The domain is automatically entered whenever a privileged process executes /usr/sbin/in.ftpd. It requires permission to execute its entry point, library files, and files located under /home/ftp/bin. It needs read and write access to devices, /home/ftp/incoming, a transfer log, and some temporary files. The domain is refused the ability to execute anything it might have written. It has permission to read under /home/ftp, /etc, and, unfortunately, the password and shadow files. However, it lacks permissions to execute files under /bin, /usr/bin, etc. Therefore all existing exploits, which require the ability to execute "/bin/cat /etc/passwd" or /bin/sh, will fail.
We now separate the ftp functionality out from the policy and into a module. The ftp module is found in Figure 3. It again defines a ftpd_d domain, and ftpd_t, ftpd_et, ftpd_xt, and ftpd_wt types. The ftpd_d definition specifies inbound domain transitions from boot_t, and from all domains defined under Admin.services. Ftpd_d may not transition to any other domains, so this access rule is absolute. This does not completely rule out ftpd_d being permitted to transition to another domain. However, in order for ftpd_d to be allowed to transition to another domain, the other domain would have to explicitly ask for ftpd_d to be permitted to transition to it, or add ftpd_d to a group and provide that group with inbound transition access.
Type ftpd_t is located under /home/ftp. Only ftpd_d may observe this type, no one may modify or execute. The file /usr/sbin/in.ftpd is the entry type through which ftpd_d may be entered, signified both by the ftpd_et type definition, and the entries line in the ftpd_d definition. The files under /home/ftp/bin, labeled as ftpd_xt, may be executed by ftpd_d, and written by root_d. There is no single domain which may both modify and execute these files. Finally, the files located under /home/ftp/incoming, labeled ftpd_wt, may be written, but not executed by ftpd_d. It may not be accessed by any other domains.
This set of accesses was also accomplished using the ftp policy. In fact, the module will eventually be compiled into a policy. However, using the module, we are able to limit statements concerning ftpd_wt to the 6 simple lines which define the type, and trust that any domains which are later added under Admin.services will be able to transition to ftpd_d.
A detailed discussion of
assert mblp protectis beyond the scope of this paper. However a brief explanation is appropriate. If no policy constraint class named mblp has been loaded, then this line will be ignored. If this class has in fact been loaded, then it is instructed to label this type, ftpd_xt, using the keyword protect. A class may do with this information what it likes. It will be called once before and once after each application of a set of modules, and given a copy of the policy at each point. The mblp class, in particular, will print a warning if any domain is in fact allowed to modify the protected type. This demonstrates the simplest use of policy consistency classes. We could in fact write a class to simply read assertions which must hold true in the policy. More interesting classes, such as mblp, compare calculations on the policy before and after module application.
The most significant advantage of separating the ftp module out from the base policy becomes apparent when we consider writing more modules. For instance, the base policy module does not allow users to change their passwords. To add this functionality, we use a module such as that in Figure 4. Nothing in the ftp module needs to change, and we do not need to consult the ftp module while writing the password module. In contrast, adding password functionality to an existing policy could become very invasive.
The simplest way to compile DTE modules into a policy is to use the command line utility dte_pc.py. A list of the modules to be applied is placed into a file, which is given as a command line argument to dte_pc.py. The resulting policy is placed into a file also specified as an argument.
Using dte_pc.py, all modules are applied simultaneously. Greater control over module application can be had on the python command line, or by writing custom module application scripts. The following python lines, for instance, combine the two modules base and user, and then apply a third module, ftp.
from DTEModule import ModuleFile # firstmods will be an array containing # the "base" and "ftp" modules firstmods = ModuleFile("base").Modules() firstmods.extend( ModuleFile("user").Modules() ) ftpmod = ModuleFile("ftp").Modules() p = DTEPolicy.Policy() # Apply "base" and "user" together p.apply_modules(firstmods) # Now apply "ftp" separately p.apply_modules(ftpmod) p.write("dte_output_file.conf")
One advantage of using this code is the enhanced precision in group definitions as described in Section 3.2. That is, if any groups are defined and referenced in base or user, and then extended in ftp, then the references to them in base and user will not include the members added in ftp. Additionally, policy consistency classes are only invoked before and after each DTEPolicy apply_modules() invocation, so the above code would force the application of ftp to be more closely scrutinized. If all modules are applied at once, then a policy consistency class will only ever compare an empty policy to the final policy, which may not be useful, depending upon the policy consistency class.
The policy language read by the DTE module is based in large part on the DTEL policy language used by the original DTE on Unix implementation . The policy consistency classes and related assert statements are a generalization of the ideas proposed in . Here Bell-LaPadula  and Strict Integrity  relations, assured pipelines , and the Clark-Wilson  concepts of constrained data items (CDIs) and transformation procedures (TPs) were used to guarantee maintenance of certain properties through dynamic policy changes. OO-DTE  applied DTE to CORBA distributed objects, and introduced an object oriented policy language, DTEL++. In OO-DTE, a user's domain was used to determine permission to execute or implement methods assigned to particular types. This is quite different from the meaning of DTE in a operating system such as Linux.
SELinux policies , like DTE policy modules, are compiled, in this case to a binary policy file. The SELinux policy makes liberal use of macros, which are defined throughout the policy, and compiled using the m4 preprocessor. SELinux policies are less structured than policy modules. There is no sense of domains and types being objects, of access rules belonging to the definition of the source of target of the definition, or of priority of conflicting access rules. SELinux policies make use of simple assert rules for safety constraints, but no attempts have been made to provide more in-depth analysis of the effects of a particular piece of policy during compilation. SELinux policies are more detailed and more complicated than DTE policies. The possibility and usefulness of transcribing the idea of policy modules to SELinux policies while keeping modules readable and small, remains to be investigated.
Tools exist to aid in editing and analyzing DTE and SELinux policies [8,12,15]. These tools analyze whole policies, and therefore complement, rather than compete with the DTE policy modules concept. The policy consistency classes used by the module compiler are designed to analyze the effect of particular policy enhancements on the overall policy. The existing DTE policy analysis tools can still be used on the policies resulting from module compilation.
IBM Research is investigating the concept of access control spaces , and working toward a method to determine whether an SELinux policy satisfies certain integrity goals . This work again analyzes whole policies. Ultimately, it is possible that Linux vendors could use this approach to verify the correctness of a TCB included with their distribution, while system administrators could use policy consistency classes to analyze the effects of their own policy modules on the base policy.
By the very virtue of being fine-grained, policies for MAC systems such as DTE and SELinux become very large, currently tens of thousands of lines for SELinux. Policy modules break this into a number of smaller pieces, and permit authors to intelligently group objects and subjects to permit concise and expressive access rules. The careful construction of a policy module language results in a far more convenient, more efficient, and safer policy specification. In practice, it has greatly eased the movement by the authors between various testing and development machines with various distributions.
Hallyn's work was supported in part by a USENIX Scholarship. The authors also wish to thank the paper shepherd, Crispin Cowan, for his helpful and constructive comments.
This work represents the view of the authors and does not necessarily represent the view of IBM. IBM is a registered trademark of International Business Machines Corporation in the United States, other countries, or both. Other company, product, and service names may be trademarks or service marks of others.
This paper was originally published in the
Proceedings of the 2004 USENIX Annual Technical Conference,
June 27-July 2, 2004, Boston, MA, USA
Last changed: 5 June 2008 jel