Author: Corey Minyard, MontaVista Software

Chapter 3: The OpenWRT Policy In Practice

In the previous two chapters (chapter 1 and chapter 2), we introduced the basics of SELinux and the OpenWRT policy. Now we will get into the details about how to actually use the policy. The policy is written so that most common operations done by many things have helper macros. You have to understand how these things are defined and used to know where to look for the helpers.

Basic CIL usage

First of all, each program (or set of programs that are related) that are confined are defined in src/agent. If you want to see the set of rules for, let’s say lxc, you could look in src/agent/lxc.cil. This example will go through the process of creating a policy for the newgidmap and newuidmap commands.

Audit Logs

First let’s look at the audit logs for this:

type=AVC msg=audit(02/17/21 06:26:07.530:64)
  avc:  denied  { execute } for  pid=927 comm=lxc-usernsexec
  name=newuidmap dev="rootfs" ino=1167
  scontext=u:r:lxc.subj tcontext=u:r:file.execfile
  tclass=file permissive=1 
----
type=AVC msg=audit(02/17/21 06:26:07.530:65)
  avc:  denied  { getattr } for  pid=927 comm=lxc-usernsexec
  path=/usr/bin/newuidmap dev="rootfs" ino=1167
  scontext=u:r:lxc.subj tcontext=u:r:file.execfile
  tclass=file permissive=1 
.
.
.
----
type=AVC msg=audit(02/17/21 06:26:07.540:75)
  avc:  denied  { open } for  pid=929 comm=newuidmap
  path=/etc/subuid dev="rootfs" ino=2114
  scontext=u:r:lxc.subj tcontext=u:r:file.conffile
  tclass=file permissive=1 
----
type=AVC msg=audit(02/17/21 06:26:07.540:76)
  avc:  denied  { getattr } for  pid=929 comm=newuidmap
  path=/etc/subuid dev="rootfs" ino=2114
  scontext=u:r:lxc.subj tcontext=u:r:file.conffile
  tclass=file permissive=1

There are a bunch more, but let’s focus on this. In these:

  • denied - These are the ones you care about. There may be other logs.
  • { xxx } This is the particular operation that was denied. What it means depends on what was denied. For files it might be open or getattr. For capabilities it will be the capability.
    comm - The program name that was running.
  • path/name/ino = The path/name/inode of the file (often relative to the filesystem base) of the object, or the name of the object. These may not be present for objects that don’t have them (unix sockets, for instance).
  • scontext - The source context trying to do the operation, almost always the subject context of the running program.
  • tcontext - The target context. Depending on the operation, it may be the object context of a file (like when accessing a file) or the subject context of a program (like when sending a signal).
  • tclass - This is an important field you need to look at; it is the class of the target. Depending on the target’s class, you have different things you have to do. Directory (dir) class is different from the file class. If you have a capability failure, cap_userns is different from capability.

With that explained, on to the actual creation of a policy for this.

Writing an Initial Policy

The first thing you should do with an audit log is ask:"Should the program be doing this?" Don't just permit everything, think about what the program should be doing. This is a big advantage over using something like AppArmor, where you can just run the program and automatically generate the policy. You may be allowing bugs in the program that you shouldn't. This is of utmost importance. Fix the program if it's doing something wrong. With that said, we can go on to actually writing policies.

You can see from the first part above that the program `lxc-usernsexec` executes the program `/usr/bin/newuidmap`. There are a lot more logs associated with this, as it has to open, read, and so on, but at this point we know what's going on. We don't want `newuidmap` to be able to see lxc types of things. We just want it to be able to do what it's designed to do. Also, we don't want lxc to be able to do the things that newuidmap can do. Therefore, we will confine newuidmap in its own subject by creating a file for it in `src/agent/newidmap.cil`:

; Allow transition from sys.subj (more or less unconfined)
    ; to newidmap context
    (in .sys
        (call .newidmap.subj_type_transition (subj)))    

    ; Allow unconfined to access newidmap files
    (in .file
        (call .newidmap.obj_type_transition_conffile
            (unconfined.subj_typeattr)))
    
    (block newidmap
        ;;
        ;; Contexts
        ;;
    
        ; Our configuration files
        (filecon
            "/etc/sub[gu]id"
            file
            conffile_file_context)
        ; Our executable files
        (filecon
            "/usr/bin/new[gu]idmap"
            file
            execfile_file_context)
    
        ;;
        ;; Macros
        ;;
    
        ; Allow access to our conf files
        (macro obj_type_transition_conffile ((type ARG1))
            (call .file.conffile_obj_type_transition
                (ARG1 conffile file "subgid"))
            (call .file.conffile_obj_type_transition
                (ARG1 conffile file "subuid")))
    
        ; Allow access to execute our programs
        (macro obj_type_transition_execfile ((type ARG1))
            (call .file.execfile_obj_type_transition
                (ARG1 execfile file "newgidmap"))
            (call .file.execfile_obj_type_transition
                (ARG1 execfile file "newuidmap")))
    
        ;;
        ;; Policy
        ;;
    
        ; Create the newidmap types and basic macros
        (blockinherit .agent.base_template)
    
        ; Create the types and macros for configuration file access
        (blockinherit .file.conf.obj_template)
    
        ; Give ourselves read access to our configuration files.
        (call read_conffile_files (subj))
    )

The first lines are pretty much standard for most programs. The `in` means we are adding things to the sys and files blocks, respectively. It's like we are appending them to it. If we create things with a new object context with filecon, we need to do one of these `(in .file)` lines so that when the file is created, it will be created with the proper labels.

The `filecon` lines set the file context for the files used by newuidmap (and newgidmap, which will also constrain as part of this). They also set the context for the executables, which we will use to set the new subject (process) context. Notice that the filenames are regular expressions.

The `(blockinherit .agent.base_template)` adds newidmap.subj to the set of subject typeattributes, and some other things. This template creates the subject type `newidmap.subj` and the executable context `execfile_file_context` we used earlier. If you want to see everything it does, you can look in `src/agent.cil` in the `agent` block and the `base_template` block.

The `(blockinherit .file.conf.obj_template)` chunk does a similar thing for configuration files. It creates `newidmap.conffile_file_context`, creates the `newidmap.conffile` type, adds that type to the `file` and `conffile` typeattributes, and creates macros that will allow subjects to access these configuration files in various ways. The typeattribute additions are what adds types to classes, and in this case the conffile and file typeattributes.

The last thing, `(call read_conffile_files (subj))`, allows our subject to access our files. No, that's not on by default.

In `src/agent/lxc.cil` we have to add some things so it can access the newidmap programs:
; We can call newxidmap executables.
    (call .newidmap.subj_type_transition (subj))

This uses the macros we created in newidmap to allow a transition to the newidmap subject and execution of the files. Now build and install the policy.

Now if we search for newuidmap in the audit files, we will see all the issues with executing it and newuidmap accessing its configuration files have gone away. But there are still issues (order slightly rearranged to group things together):

type=AVC msg=audit(02/17/21 19:42:49.570:80)
      avc:  denied  { use } for  pid=892 comm=newuidmap
      path=pipe:[2178] dev="pipefs" ino=2178
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=fd permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.570:81)
      avc:  denied  { write } for  pid=892 comm=newuidmap
      path=pipe:[2178] dev="pipefs" ino=2178
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=fifo_file permissive=1

These have to do with the fact that lxc has opened a pipe so it can grab the output of newidmap. You can tell because the tcontext is lxc.subj, meaning it belongs to lxc. Elsewhere in the logs it had also passed another pipe that needed read, so we need read access, too. We have to give access to this in lxc.cil:

    ; We pass a pipe to newxidmap, let it have access
    (in .newidmap
        (call .lxc.readwriteinherited_fifo_file (subj))
    )

Now on to /proc:

    type=AVC msg=audit(02/17/21 19:42:49.580:85)
      avc:  denied  { read } for  pid=892 comm=newuidmap
      name=891 dev="proc" ino=2179
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=dir permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:86)
      avc:  denied  { open } for  pid=892 comm=newuidmap
      path=/proc/891 dev="proc" ino=2179
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=dir permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:94)
      avc:  denied  { write } for  pid=892 comm=newuidmap
      name=uid_map dev="proc" ino=2183
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=file permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:93)
      avc:  denied  { getattr } for  pid=892 comm=newuidmap
      path=/proc/891 dev="proc" ino=2179
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=dir permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:95)
      avc:  denied  { open } for  pid=892 comm=newuidmap
      path=/proc/891/uid_map dev="proc" ino=2183
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=file permissive=1

This is the program that lxc is asking us to set the uid map for. This brings up a bit of a security quandary. We have the newuidmap program writing to the `uid_map` file in `/proc`, which is arguably its job. However, we are doing this for a program that is an lxc program, so newidmap will have access to do this for lxc files. 

You could let the newuidmap program run under the `lxc.subj` context. But then you would have to give `lxc.subj` access to the `newidmap` files, and anything else `newuidmap` needed. The current analysis says that it's important to not run outside things as `lxc.subj`, since that has access to the container internals and that's the most important thing to protect. Therefore, we will go ahead with creating the `newidmap.cil` file, but it may not be the best policy, or it may be ok for this application but not in general.

So we have to allow access to this in lxc.cil, newidmap doesn't know where it's coming from. We have to add something in the lxc block to give access. We can't use the standard procfile macros because they work on the standard file context, lxc.fs, not the executable context, lxc.subj in this case:

    ; This allows us to pass proc object to newidmap because the /proc/nnn
    ; files for things I execute will be owned by lxc.subj.
    (macro list_procsubj_dirs ((type ARG1))
        (allow ARG1 subj list_dir))
    (macro readwrite_procsubj_files ((type ARG1))
        (allow ARG1 subj readwrite_file))

And now tell newidmap it has access, again in lxc.cil:

    ; Let newxidmap access our /proc/nnn files to set the uid map
    (in .newidmap
        (call .lxc.list_procsubj_dirs (subj))
        (call .lxc.readwrite_procsubj_files (subj))
    )

And finally, /proc itself is a procfile, so give access to that in newidmap.cil:

    ; Allow access to list files in /proc
    (call .fs.list_procfile_dirs (subj))

The rest are changes to newidmap.cil:

    type=AVC msg=audit(02/17/21 19:42:49.580:87)
      avc:  denied  { create } for  pid=892 comm=newuidmap
      scontext=u:r:newidmap.subj tcontext=u:r:newidmap.subj
      tclass=unix_stream_socket permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:88)
      avc:  denied  { connect } for  pid=892 comm=newuidmap
      scontext=u:r:newidmap.subj tcontext=u:r:newidmap.subj
      tclass=unix_stream_socket permissive=1 

We have to allow access to unix sockets:

    ; Allow us to create and connect to a unix socket
    (allow subj self create_unix_stream_socket)

And more issues:

    type=AVC msg=audit(02/17/21 19:42:49.580:89)
      avc:  denied  { search } for  pid=892 comm=newuidmap
      name=run dev="rootfs" ino=1263
      scontext=u:r:newidmap.subj tcontext=u:r:varfile.runtimevarfile
      tclass=dir permissive=1 

This is a little tricky. But you can tell from `runtimevarfile` that this is in /var, and the name is run, so it's trying to search /var/run. Easy enough, give access to search varfile.runtime:

    ; Allow search on /var/run
    (call var.search_fs_dirs (subj))

And now more:

    type=AVC msg=audit(02/17/21 19:42:49.580:90)
      avc:  denied  { read } for  pid=892 comm=newuidmap
      name=passwd dev="rootfs" ino=1239
      scontext=u:r:newidmap.subj tcontext=u:r:nameservice.miscfile
      tclass=file permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:91)
      avc:  denied  { open } for  pid=892 comm=newuidmap
      path=/etc/passwd dev="rootfs" ino=1239
      scontext=u:r:newidmap.subj tcontext=u:r:nameservice.miscfile
      tclass=file permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:92)
      avc:  denied  { getattr } for  pid=892 comm=newuidmap
      path=/etc/passwd dev="rootfs" ino=1239
      scontext=u:r:newidmap.subj tcontext=u:r:nameservice.miscfile
      tclass=file permissive=1 

newuidmap needs to read from /etc/passwd. Other things do this, so there are probably already macros to handle this. If we hunt around, we will find some macros in `file/miscfile/nameservicesmiscfile.cil` in the `nameservice` block. We can use that:

    ; Allow access to /etc/passwd
    (call nameservice.read_miscfile_files (subj))

And some capability issues:

    type=AVC msg=audit(02/17/21 19:42:49.580:96)
      avc:  denied  { sys_admin } for  pid=892 comm=newuidmap
      capability=sys_admin
      scontext=u:r:newidmap.subj tcontext=u:r:newidmap.subj
      tclass=cap_userns permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.580:97)
      avc:  denied  { setuid } for  pid=892 comm=newuidmap capability=setuid
      scontext=u:r:newidmap.subj tcontext=u:r:newidmap.subj
      tclass=capability permissive=1
    ----
    type=AVC msg=audit(02/17/21 19:42:49.590:98)
      avc:  denied  { setgid } for  pid=893 comm=newgidmap capability=setgid
      scontext=u:r:newidmap.subj tcontext=u:r:newidmap.subj
      tclass=capability permissive=1 

These just need a basic allow:

    ; Capabilities we need to set the uids and guids
    (allow subj self (cap_userns (sys_admin)))
    (allow subj self (capability (setuid setgid)))

So we go through all this, get everything working, and look again:

    type=AVC msg=audit(02/17/21 22:58:46.460:139)
      avc:  denied  { read write } for  pid=929 comm=newuidmap
      path=socket:[2241] dev="sockfs" ino=2241
      scontext=u:r:newidmap.subj tcontext=u:r:lxc.subj
      tclass=unix_stream_socket permissive=1

This was further down the file, probably from another invocation of newuidmap. For some reason it's inheriting a unix stream socket from lxc. That's probably a bug in lxc, not closing the socket in the fork before the exec, but we can suppress the warning. If newuidmap doesn't use it, then it won't matter. In the `in .newidmap`, add the following:

    ; This appears to be due to a bug in lxc, it's not closing a socket
    ; that gets passed when newxidmap is called.  Suppress the log
    ; as we know about this.
    (dontaudit subj .lxc.subj readwrite_unix_stream_socket)

There's not a macro for this, so we have to hand-code it.

It's not that easy

Note that I've made this look easy. It's not. I spent an entire day working on this. Then the OpenWRT maintainer went through it and told me all kinds of things I had done wrong. You have to understand security. You have to dig, look at things, and figure things out. Things often don't work like you expect and you have to play with them. The compiler messages could be more useful (though they are much better than the old policy compiler) . It's best to work a little bit at a time and recompile so you know what changed to cause an issue.

The Various Pieces and How They Work

The basic mechanisms here that do most of the work are the `macro`, `call`, and `block` operations. Then there are the `in` and `blockinherit` operations.

Note that these functions are position independent. You do not have to declare macros before they are used in the file. In general, order is irrelevant. There are corner cases, like when you override a macro.

macro and call

A `macro` is what you would expect, like C macros, what's in the macro gets put where the `call` statement is, with the arguments substituted. It's pretty straightforward.

block and blockinherit

A block creates a namespace and allows operations in that namespace. This namespace performs multiple functions depending on context.

namespace blocks

Some blocks, like our `newidmap` block above, are used to create a namespace for defining subject and object contexts. You will notice the `subj` used a lot, this is created by the

    (blockinherit agent.base_template)

and is the subject context in the block. This particular blockinhert does a huge number of things, and is one of the reasons we used the openwrt policy as a starting point. There would have otherwise been a boatload of things to write. You can look in `src/agent.cil` and follow all the things it does.

When you do things like:

    (blockinherit .file.conf.obj_template)

You are pulling in the `obj_template` block that is inside the `conf` block that is inside the `file` block. It will create a conffile object based upon the block name, in our `newidmap` case `newidmap.conffile`. The above statement will create `conffile_file_context` in the block's namespace. Macros defined in a block are available under that block name, along with all the types created. For instance, if we needed to reference the newidmap conffile context, we could use `.newidmap.conffile_file_context`.

template blocks

These are blocks used by `blockinherit`, they are basically templates that are inserted in place that use the current block name to customize them for where they are inserted. It's pretty much a direct insertion, if I have a block that is:

    (block asdf
        (blockabstract asdf)
        (macro1 m1 ....)
        (macro2 m2 ....))

and I use it in another block:

    (block jkl
        (blockinherit asdf))

it would be as if it was:

    (block jkl
        (macro1 m1 ....)
        (macro2 m2 ....))

The `blockabstract` statement declares that the block is only designed to be blockinherited; it will not generate any direct code from the block.

in

The in statement appends to an existing block. For instance, we had:

    (in .newidmap
        (call .lxc.readwriteinherited_fifo_file (subj))
    )

in our `lxc.cil` file. This is because you wouldn't want to add something to `newidmap.cil` every time something used it. So you can instead add it in the file that uses it, since the file that uses it knows it's using it.

The `in` statement just sticks the contents into the block as-is. This means that the `subj` is the subj in the target block, making it easier to use.

Stay tuned for our next blog chapter!

-----

Learn more about MVSecure services and MVXpert services.