Author: Corey Minyard, MontaVista Software

Chapter 2: Implementing the Policy

In the previous chapter we talked about the requirements and our initial path on our SELinux project. Now let’s discuss the actual implementation.

Introducing SELinux

If you’ve never used SELinux, I’m going to refer you to The SELinux Notebook or SELinux By Example. They use the old SELinux kernel language, but the basic concepts apply. The reference for the new language, the CIL language, is CIL (Common Intermediate Language). I will try to summarize the basics of SELinux, though it could be inadequate for properly implementing this policy in practice. Please read the documents in the links above for a full understanding.

SELinux provides Mandatory Access Control (MAC). “Mandatory” doesn’t mean mandatory; it means centralized. In a MAC system you typically have one centralized access control policy, not policy done by each user on their own files. This terminology comes from security parlance; SELinux was designed by people who work in security systems, and some of the terms are based on old security concepts, not software concepts.

SElinux works on a default no permission model. By default nothing is permitted. Permissions must be added to allow things, and hence done in excruciating detail. Permissions are provided via a context for subjects and objects. Subjects are things that attempt to access objects. Subjects are generally programs; objects are generally everything else (files, sockets, pipes, capabilities, etc.).

A context consists of at least three parts: a user, a role, and a type. There are other parts that can be added that deal with Multi-Level Security (MLS), which is more of a military grade policy and not used in this policy. In this policy, since there is really only a single user and roles, the policy only has a single user and role that are set to `u` and `r` for all contexts. It essentially only uses the type.

The type is used in a number of ways. A device will have an object context. For instance, /dev/null has a unique context u:r:null.nodedev:

# ls -Z /dev/null
u:r:null.nodedev                 /dev/null

The contexts of files in the filesystem are called labels, and the process of setting these contexts is called labeling. The labeling of files (and directories) is specified in the policy and applies to both static files on the filesystem and files that are dynamically created.

If a program needs to use /dev/null, a rule must be added to permit the access. All rules are based upon context. So the program’s subject context has to be allowed for the specific context. For instance, if a program running as lxc (say lxc-create) in the context u:r:lxc.subj needs to write to /dev/null, there would need to be a rule:

(allow lxc.subj null.nodedev (chr_file (open write)))

If a program needs to execute another program that runs under a different context, it is called a transition. Subject transitions must be allowed typically via a rule and specified in a typetransition statement. This requires allowing read, execute and mmap access to the file, similar access to load the shared libraries, the ability to get the SIGCHLD from the child, and some other things of that nature. 

Basics of the OpenWRT Policy 

However, you don’t normally work at that low a level when using a policy. The OpenWRT policy provides lots of macros and boilerplate that make referring to these things simpler and easier to maintain. For instance, in this case of access to /dev/null, the actual thing we would do is add the following to the lxc block in src/agent/lxc.cil:

(call .null.write_nodedev_chr_files (subj))

Let’s dissect this a little further. If we look in src/dev/nodedev/nullnodev.cil, we will find a reference to /dev/null. It has the following statement after the filecon and macro statements that refer to /dev/null:

  (blockinherit .dev.node.obj_template)

The blockinherit means we take the contents of the block named obj_template that is in block node that is in block dev. We will find that block (grep for “block node”) in src/dev/nodedev.cil.

(in .dev

    (block node

The in statement here means: “tack stuff onto the end of the dev block.” Then at the end of this file:

(block obj_template
        (blockabstract obj_template)

        (blockinherit .dev.node.obj_base_template)
        (blockinherit .dev.node.obj_macro_template))))

The obj_base_template is in this file:

(block obj_base_template

        (context
         nodedev_file_context
         (.u
          .r
          nodedev
          (systemlow
           systemlow)))

        (blockabstract obj_base_template)

        (type
         nodedev)

        (call .dev.node.obj_type (nodedev)))

This particular piece creates the null.nodedev type, and the null.nodedev_file_context context that is u:r:null.nodedev. (The systemlow thing is for multi-level security, which we don’t use.)

The call statement calls a macro named obj_type. If we look in nodedev.cil, where you would expect it, it’s not there. So it must be in a blockinherit in that file. There are a couple, searching for macro obj_type and finding which one is the most likely, we find one in file.cil that looks promising, and indeed, we have:

(blockinherit .file.obj_all_macro_template)

in nodedev.cil. In file.cil, we have:

(macro obj_type ((type ARG1))
       (typeattributeset obj_typeattr ARG1))

This adds the null.nodedev type to dev.obj_typeattr.

Now if we look in obj_macro_template in nodedev.cil, we find a bunch of macros. If we look in these macros, we will find:

(macro write_nodedev_chr_files ((type ARG1))
        (allow ARG1 nodedev write_chr_file))

So doing our substitution here back to the original call, we get:

(allow lxc.subj null.nodedev write_chr_file)

If we expand all these, we get:

(allow lxc.subj null.nodedev
     (chr_file (append getattr ioctl lock open write)))

However, all this is unnecessary, because lxc.cil has:

(blockinherit .agent.base_template)

which will eventually result in:

(call .null.readwrite_nodedev_chr_files (subj_typeattr))

from the .subj.common block, which is one of the many things that the agent base template does. So normally you don’t have to add specific access to /dev/null as it comes by default.

Helper programs and tools

On the target system, if logs are going to the standard audit location (/var/log/audit/audit.log) there is a tool named ausearch that makes things a little easier to read. You can do:

ausearch -i | less

and get some output with rather long lines. To make it easier to read, we can break it into lines using sed:

ausearch -i | sed -e 's/ : /\n  /' -e 's/ scontext=/\n  scontext=/'\
    -e 's/ tclass=/\n  tclass=/' -e 's/ name=/\n  name=/'\
    -e 's/ path=/\n  path=/' | less

And you get some more neatly formatted output:

----
type=AVC msg=audit(02/15/21 22:07:52.490:34)
  avc:  denied  { write } for  pid=860 comm=sm_manager
  name=tmp dev="rootfs" ino=1261
  scontext=u:r:smm.subj tcontext=u:r:tmp.fs
  tclass=dir permissive=1 
----

Stay tuned for our next blog chapter!

-----

Learn more about MVSecure services and MVXpert services.