An Experience in Using SELinux in an Embedded System - Chapter 2
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.