Table of Contents

1. Access Control

On the Linux desktop, most people are running apps with zero access control enforced, meaning that running the Discord client for example, can rm -rf ${HOME} or slurp up your private data such as your ssh keys. This is not great, but there are several options available to address this problem.

Some of the information I shared here could be incorrect. Please contact me and let me know if anything is wrong, I am not an expert on any of these subjects.

2. Example Program

I'm going to use weechat as an example of a program that we want to isolate from the rest of the system. It's simple but not trivial, and touches a lot of commonly shared directories like XDG_CONFIG_DIR, XDG_STATE_DIR and similar.

At it's core, Weechat needs access to a few directories and the network. We will focus on the directories.

Here are the directories/files it needs write access to:

${HOME}/.config/weechat
${HOME}/.cache/weechat
${HOME}/.local/share/weechat
${HOME}/.local/state/weechat
${XDG_RUNTIME_DIR}/weechat

It also needs read-only access to some system files such as (assuming a merged-usr system):

/etc/ld.so.cache # dynamic loader cache
/usr/lib{,32,64}
/usr/bin/weechat # weechat executable itself
/usr/share

3. Our Options

There are multiple options available to do access control on Linux, but I'm going to cover namespaces (with bubblewrap), Apparmor and SELinux.

3.1. Bubblewrap

Bubblewrap is a small C utility used to setup mount namespaces for sandboxing and container purposes.

Mount namespaces are a way to control processes views of mount points, meaning processes in different mount namespaces cannot see each others mounts. A simple example would be mounting a USB drive to /mnt/usb. If you mount the USB drive in a separate mount namespace, other processes will not see anything mounted to /mnt/usb at all.

Bubblewrap works by creating a new mount namespace, and then creates a new root mountpoint on a tmpfs (similar to a chroot), and bind mounts in the directories and files provided by the command line parameters.

Let's see an example of how we actually do this:

#!/bin/bash

args=(
  --unshare-all
  --share-net
  --dev  /dev
  --proc  /proc
  --tmpfs /tmp
  --tmpfs /run
  --tmpfs /var
  --tmpfs /mnt/sandbox
  --ro-bind /etc/ld.so.cache /etc/ld.so.cache
  --ro-bind /usr   /usr
  --ro-bind /bin   /bin
  --ro-bind /sbin/ /sbin
  --ro-bind /lib   /lib
)

# handle lib32 and lib64 for some systems
[[ -e /lib32 ]] && args+=(--ro-bind /lib32 /lib32)
[[ -e /lib64 ]] && args+=(--ro-bind /lib64 /lib64)

exec bwrap ${args[@]} /bin/sh

Running this script should drop you into a shell in the sandbox.

You won't be able to access much since almost everything is mounted read only, but there are writable tmpfs mounts. The tmpfs mount points will not persist across runs, and get deleted when the sandbox is destroyed.

This isn't super useful but it shows a simple example. Now lets adapt this to run weechat!

#!/bin/bash

# setup the core bind mounts
args=(
  --unshare-all
  --share-net
  --dev     /dev
  --proc    /proc
  --tmpfs   /tmp
  --tmpfs   /run
  --tmpfs   /var
  --tmpfs   /mnt/sandbox
  --ro-bind /etc/ld.so.cache  /etc/ld.so.cache
  --ro-bind /usr   /usr
  --ro-bind /bin   /bin
  --ro-bind /sbin/ /sbin
  --ro-bind /lib   /lib
)

# handle lib32 and lib64 for some systems
[[ -e /lib32 ]] && args+=(--ro-bind /lib32 /lib32)
[[ -e /lib64 ]] && args+=(--ro-bind /lib64 /lib64)

# weechat specific bind mounts (make sure these exist before running the script)
args+=(
  --tmpfs ${HOME}
  --bind  ${HOME}/.config/weechat      ${HOME}/.config/weechat
  --bind  ${HOME}/.cache/weechat       ${HOME}/.cache/weechat
  --bind  ${HOME}/.local/share/weechat ${HOME}/.local/share/weechat
  --bind  ${HOME}/.local/state/weechat ${HOME}/.local/state/weechat
)

exec bwrap ${args[@]} /usr/bin/weechat

Hopefully weechat starts up. Now it will only have read only access to most of the system, and will not be able to access anything else in your ${HOME}, such as your ssh keys.

You may want to adapt this script to bind in other things, but this should at least give you a start.

There are some caveats with bwrap based sandboxing. The primary issue is that it requires "root" to create mount namespaces. You might wonder why you were able to run without root before, this is because bwrap created a user namespace.

User namespaces are similar to mount namespaces, but they unshare IDs rather than mount points. This means you can become UID 0 (root) inside of a sandbox, and perform actions that normally require root access, but outside of the sandbox you are still not-root and have no extra privileges.

User namespaces involve ID mapping. For example, UID 1000 may be mapped to UID 0 inside of the container. Most Linux systems also have a reserved range of IDs for each user, dedicated for mapping into user namespaces. My system has notroot:100000:65536 dedicated for user notroot. So all UIDs between 100000 and 165536 are reserved for this purpose. If you map 1000:0:1 and 100000:1:65535, files created inside of the sandbox by root will appear as owned by UID 1000 outside, and files owned by UID 1000 in the sandbox will be seen as UID 100999 outside. IDs that are not mapped will be seen as "nobody" inside of the sandbox.

ID mapping is confusing for me personally, but bwrap has some flags to help you setup trivial mappings that should work for a lot of simple use cases.

bwrap can also unshare ipc, pid, net, uts and cgroup namespaces, which all work similar to the namespaces described above, and provide isolation for things beyond files which is also an important aspect of sandboxing.

3.2. Apparmor

Apparmor is a "Linux Security Module" (LSM), and a mandatory access control (MAC) system.

MAC is different from discretionary access control (DAC) in that a central authority controls the rules, instead of owners of the resource.

Apparmor is a path based LSM. Apparmor profiles define a list of paths that a process can or can't access. The profile syntax supports glob-like "patterns" for matching specific paths that the process might try to access as runtime.

Lets show an example of an Apparmor profile for our IRC client:

#include <tunables/global>

profile weechat /usr/bin/weechat {
  #include <abstractions/base>

  # read only shared system resources
  /etc/fonts/** r,
  /usr/share/** r,

  owner @{HOME}/.config/weechat/ rw,
  owner @{HOME}/.config/weechat/** rw,

  owner @{HOME}/.cache/weechat/ rw,
  owner @{HOME}/.cache/weechat/** rw,

  owner @{HOME}/.local/share/weechat/ rw,
  owner @{HOME}/.local/share/weechat/** rw,

  owner @{HOME}/.local/state/weechat/ rw,
  owner @{HOME}/.local/state/weechat/** rw,

  owner @{XDG_RUNTIME_DIR}/weechat/ rw,
  owner @{XDG_RUNTIME_DIR}/weechat/** rw,     
}

The first part of the profile simply includes a file (via the c-pre-processer) that has "tunables" such as @{HOME} predefined.

The second part of the profile (the profile weechat part) defines a profile for the /usr/bin/weechat executable. Apparmor transitions into confined mode when a process executes an executable that matches the /usr/bin/weechat pattern (globs are supported here).

The third part of the profile includes the base abstraction. base gives access to all of the basic things all processes will need to run at all, such as access to /usr/lib or /dev/null. You can technically define these all yourself, but it's quite a lot of boilerplate, and the base should work for most use cases.

The rest of the profile defines path and patterns and access rules for them. Weechat will only be able to access the paths you defined and the things defined in base with this profile.

Apparmor is very simple and easy to get started with, but does have a few flaws.

The primary flaw is that apparmor is path based rather than inode based. Hardlinks of files could allow bypassing the apparmor rules, depending on the exact situation. Apparmor disallows creating links by default though, so the hardlinks would have to be created by something unconfined or that was explictly allowed.

By default, apparmor prevents you from being able to execute the paths you gave access to. There are a few ways to give execute permissions.

3.2.1. Execute Modes

  • ix starts the subproc under the current profile
  • ux starts the subproc unconfined
  • px starts the subproc under a profile that matches the executable path
  • cx starts the subproc under a subprofile

3.2.2. Caveats

Until Linux 6.17, apparmor will not be fully functional without Ubuntu kernel patches.

The primary missing feature I am aware of is the ability to restrict access to unix sockets.

3.3. SELinux

Selinux is another MAC based LSM, it's however quite different from apparmor.

3.3.1. Labels

Selinux access control works by labeling subjects (processes) and objects (files etc) with "types", this information is stored in the files xattrs, an example label is "sys.id:sys.role:sys.subj:s0".

Unlike apparmor, Selinux is inode based rather than path based, so hardlinks can't be used as loopholes.

The first part of the label is the user, the second is the role and the third is the type. Mostly we are going to ignore users and roles and focus on types for this.

3.3.2. Commands & Utils

The Selinux userland comes with many utilites and figuring out what they do and why you would want them is not easy to figure out.

  1. sestatus

    sestatus is a simple command that tells you whether SELinux is currently active, and whether it's in permissive or enforcing mode. There isn't much more to it, but it's handy to detect if SELinux is currently active.

  2. restorecon

    restorecon applies filecon rules to your files. filecon is an expression in policy like this:

    (filecon "/home/john/.*")
    

    These expressions are compiled and the end result is a file called file_contexts, and normally installed into the policy config (e.g /etc/selinux/${SELINUXTYPE}).

    The modular policy system also keeps track of filecon expressions, so you don't need to change the policy config files everytime you want to update the rules.

    Using restorecon:

    # recursivly apply file contexts to the entire filesystem
    restorecon -Rv /
    
    # restore a single file
    restorecon -v /home/john/foo.txt
    
  3. setfiles

    setfiles uses the file_contexts file mentioned before to label mountpoints. The default context for files is inherited from the mountpoint (afaik this is how it works?).

    When using setfiles, you probably want to bind mount your root filesystem somewhere, like /mnt/gentoo. Otherwise you may not apply the contexts to the mount points themselves.

    Hint: BTRFS subvolumes also count as mount points, and nested subvolumes can be a little confusing

    This is how I used setfiles for my system:

    setfiles -v \
      -r /mnt/gentoo \
      /etc/selinux/${SELINUXTYPE:-dssp5}/contexts/files/file_contexts \
      /mnt/gentoo/{,dev,proc,run,sys,tmp,boot,efi,etc,var,home} \
      /mnt/gentoo/mnt/subvolumes/var/{cache,tmp} \
      /mnt/gentoo/mnt/subvolumes/home/notroot \
    

    I have the following subvolumes:

    /mnt/subvolumes/etc
    /mnt/subvolumes/var
    /mnt/subvolumes/var.cache
    /mnt/subvolumes/var.tmp
    /mnt/subvolumes/home
    /mnt/subvolumes/home.notroot  
    

    Some of the subvolumes end up mounted on top of each other, like /mnt/subvolumes/home is mounted at /home, and /mnt/subvolumes/home.notroot is mounted at /home/notroot, so this means the "raw mount point" is actually /mnt/gentoo/mnt/subvolumes/home/notroot not /mnt/gentoo/home/notroot. This is pretty confusing and easy to get wrong.

  4. getpathcon and matchpatchon

    matchpathcon reads your file_contexts and shows you the default label for the paths provided.

    matchpathcon /home/john
    matchpathcon '/var/log/.*'
    

    getpathcon just gets the current context for a file.

    getpathcon /home/john
    
  5. semodule

    SELinux can load policy in two different ways. "monolithic" and "modular". Monolithic loading is mostly designed for embedded systems and can be ignored for now.

    semodule is an interface to the "modular" SELinux policy store. You can load modules at runtime, dynamically, and even version control modules.

    You can load cil files directly with semodule, each cil file corresponds to a single module. Modules loaded with with semodule are stored at /var/lib/selinux/${SELINUXTYPE}/active/modules/.

    Hint: you can't have two cil files with the same name even if they are in different directories without clobbering your modules.

    List all currently install modules:

    semodule -l
    

    Load modules:

    semodule -i foo.cil bar.cil baz.cil
    

    Remove modules:

    semodule --remove foo bar baz 
    

3.3.3. Dssp5

This post is going to assume we are basing our policy dssp5, a minimal and modular base policy that we create our own types on top of. dssp5 provides the core types.

3.3.4. Built In Types

dssp5 provides many core types that we will build our policy on top of.

An example of a core type is home.file. This is a type applied to home directories such as /home/john. There are many base types for various parts of the filesystem.

Here are some major built in types:

  • conf.file for /etc
  • lib.file for /usr/lib
  • exec.file for /usr/bin
  • run.file for /run
  • var.file for /var

There are also "subtypes" for some of these built in types like spool.var.file for /var/spool.

3.3.5. How Do Files Get Typed

  1. Setfiles

    Mount points will be labeled with setfiles, and any new files created underneath that mount point should inherit the label by default. This is default label for files that don't have a filecon defined.

  2. Filecon

    In Selinux policy you will define filecon expressions like this (ignore the other parts for now):

    (block var
      (blockherit .file.template)
      (filecon "/var" dir file_context)
      (filecon "/var/.*" file file_context))  
    

    After compiling and loading the policy, you would use the built in restorecon command to apply these labels.

  3. Type Transitions

    Also files can change types via type transitions at runtime. An example for weechat, we want all of the runtime files weechat creates to be labeled agent.weechat or similar, so we define a type transition in the weechat selinux module:

    (call .agent.weechat.run.file_type_transition_file (.agent.weechat.subj dir "weechat"))
    (call .agent.weechat.run.file_type_transition_file (.agent.weechat.subj file "*"))  
    

    (Don't worry if you don't understand this yet, we will learn more about the cil language in a bit.)

    Another example would be transitioning from one context to another when executing something. In our later policy, running the weechat executable causes a type transition from sys.subj to weechat.subj.

3.3.6. How Do Processes Get Typed

With dssp5, processes will start in the sys.subj context which is basically unconfined and has access to everything. Processes change types via type transitions or with runcon. We will go over type transitions a bit more later when we define the weechat module.

(sidcontext init (sys.id sys.role sys.subj sys.lowlow)) ;; userspace_initial_context

3.3.7. Cil Overview

Cil is the language we will write policy in. It's a simple sexpr based language, with namespaces, types, typeattributes (metatypes), macros and templates.

  1. Cil Types

    We can define types like this:

    (type foo)
    
  2. Cil Namespaces

    In cil we will almost always be working in a namespace.

    We can define a namespace with the block keyword:

    (block foo
      (block bar))
    

    If a block has already been created and you want to "enter" it, you use the "in" keyword

    (in .foo.bar)
    

    You access types with the . operator. A dot at the beginning of the expression starts searching from the "top" namespace rather than looking for a type in the current namespace.

    (in foo.bar
      (macro baz ((type ARG1))
        (do_something_with ARG1))
    
      ;; define a type
      (type qux)
    
      ;; call our macro using local lookup
      (call baz (qux))
    
      ;; call our macro using global lookup
      (call .foo.bar.baz (.foo.bar.qux))
    

    We will make great use of namespaces in our policy!

  3. Macros

    Macros are sort of like functions. Macros "capture" local types similar to lambdas and interpolate parameters into expressions.

    (block foo
      (type bar)
    
      ;; define our macro (we will cover typeattributes soon)
      (macro test ((type ARG1))
        (typeattributeset bar ARG1)))
    
    (block baz
      (type qux)
      ;; call our macro
      (call .foo.test (qux)))
    
  4. Templates

    Templates are blocks that are inherited by other blocks.

    Abstract blocks are blocks which only exist once they are inherited.

    You can think of abstract blocks like inheritance and OOP in programming.

    (block foo
      ;; define our abstract block (template)
      (block bar
        (blockabstract bar)
        ;; define a type
        (type t)))
    
    (block baz
      ;; inherit the bar block, now the t type will be created and in scope
      (blockinherit .foo.bar)
      (dothing t))
    

    Hint: abstract blocks are very commonly used to define types, so you will often not be defining (type foo) directly, but instead letting the templates do the work for you.

    We will make great use of the built in templates for almost everything we do.

  5. Type Attributes

    Type attributes are like "metatypes". They are used to group types together for shared behaviour.

    An example here:

    (in file
      (block user
        (macro type ((type ARG1))
          ;; since its a macro we can use things before they are defined
          (typeattributeset typeattr ARG1))
    
        ;; create the type attribute
        (typeattribute typeattr)
    
        ;; our typeattr can be associated with another one as well
        (call .file.home.type (typeattr))
    
        (block base_template
          (blockabstract base_template)
          (blockinherit .file.base_template)
          ;; remember the file type is introduced via the template
          ;; associate the file type with the userfile.typeattr
          (call .userfile.type (file)))))
    
    (block ssh
      (blockinherit .file.user.template
      ;; file and file_context are also introduced via the .userfile.base_template, which inherits
      ;; from .file.base_template (layers of templates like this is important for
      ;; abstracting out boilerplate)
      (filecon "HOME_DIR/\.ssh" dir file_context)
      (filecon "HOME_DIR/.ssh/.*" file file_context)))
    
    (block gpg
      (blockinherit .file.user.template
      (filecon "HOME_DIR/\.gnupg" dir file_context)
      (filecon "HOME_DIR/.gnupg/.*" file file_context)))
    
    ;; Now we can give something access to all userfiles instead of listing each type.
    (block userdel
      (blockinherit .subj.template)
      ;; allow access to all userfiles including ssh and gpg files
      (call .file.user.type (subj)))
    

    A good example for the usefulness of type attributes is the program userdel, this needs access to ${HOME} and all user files underneath. If each type (ssh, gpg, foo) were not associated with the file.home.typeattr (via associating with .userfile.typeattr), policy for userdel would need to manually allow each type to do it's job.

    Typeattributes are one of the most important things for abstracting out behavior. You can create hierarchies of types in a way similar to OOP.

  6. Type Transitions

    Type transitions are rules in policy that control how types change at runtime. A common desire would be to have files created by weechat end up with a weechat label, or entering a new context when executing something.

    I do not fully understand how these work internally, but I will show an example of how to do this:

    (block weechat
      (block run
        (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
          (call .user.run.file_type_transition (ARG1 file ARG2 ARG3)))
    
        ;; inherit the template for files in /var/user/${UID}
        (blockinherit .file.user.run.template)))
    
    (call .agent.weechat.run.file_type_transition_file (.agent.weechat.subj dir "weechat"))
    (call .agent.weechat.run.file_type_transition_file (.agent.weechat.subj file "*"))
    

    This will cause files created in the weechat context, under /var/run/${UID} to be transitioned into the agent.weechat.run.file type, rather than the "default" user.var.file (the default type depends on your policy, this is just an example).

3.3.8. Policy

Lets write some policy now!

  1. XDG Directories

    We want to create some new types for the directories weechat requires access to.

    ;; create out xdg namespace
    (block xdg
        ;; we will create a subnamespace for each xdg file type (e.g config, cache, share, state)
        (block config
            ;; this next block isn't technically required but it shows that we are a subtype of .file.home
            (block home
                ;; create a macro to allow type transitions for files in our context
                (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                    (call .home.file_type_transition_file (ARG1 file ARG2 ARG3)))
    
                ;; inherit the template which defines some types for us and also provides some macros
                (blockinherit .file.home.template)
    
                ;; define a context for the ~/.cache directory itself
                ;; hint: HOME_DIR is one of the few variables that can be interpolated into strings
                (filecon "HOME_DIR/\.config" dir file_context)
                (filecon "HOME_DIR/\.config/.*" file file_context)))
    
        (block cache
            (block home
                (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                    (call .home.file_type_transition_file (ARG1 file ARG2 ARG3)))
    
                (blockinherit .file.home.template)
    
                (filecon "HOME_DIR/\.cache" dir file_context)
                (filecon "HOME_DIR/\.cache/.*" file file_context)))
    
        (block share
            (block home
                (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                    (call .home.file_type_transition_file (ARG1 file ARG2 ARG3)))
    
                (blockinherit .file.home.template)
    
                (filecon "HOME_DIR/\.local/share" dir file_context)
                (filecon "HOME_DIR/\.local/share/.*" file file_context)))
    
        (block state
            (block home
                (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                    (call .home.file_type_transition_file (ARG1 file ARG2 ARG3)))
    
                (blockinherit .file.home.template)
    
                (filecon "HOME_DIR/\.local/state" dir file_context)
                (filecon "HOME_DIR/\.local/state/.*" file file_context))))
    
  2. Loading Policy

    You can load dssp5 policy up with:

    make modular_install
    

    Next run restorecon to apply our new labels (this could take a while):

    restorecon -Rv /
    

    If everything went as planned you should be able to use ls -alZ ${HOME} to see your new labels.

  3. Weechat Policy

    Define policy for weechat itself:

    ;; Selinux is deny by default. So we must explicitly allow access to everything weechat needs,
    ;; including system libraries, the dynamic loader, xdg directories and more.
    
    ;; reserve ports for irc purposes
    (block irc
        (portcon "tcp" 6667 port_context)
        (portcon "tcp" 6697 port_context)
        (blockinherit .net.port.unreserved.template))
    
    ;; define our weechat namespace
    (block weechat
        ;; most or all things have a template defined that we can use
        ;; rarely do we write policy "from scratch", this includes subjects which are
        ;; what we are creating here
        (blockinherit .subj.template)
    
        ;; authorize sys.role to access the subj domain
        (roletype .sys.role subj)
    
        ;; allow signaling ourself
        (allow subj self (process (fork sigchld sigkill signal signull sigstop)))
    
        ;; allow setattr and getattr on our own files
        (allow subj self (file (setattr getattr)))
    
        ;; allow reading and executing our own binary (/usr/bin/weechat)
        ;; also allow a type transition from .sys.subj to .weechat.subj
        (call .weechat.exec.subj_type_transition (.sys.subj subj))
        (call .weechat.exec.entrypoint_file_files (subj))
        (call .weechat.exec.mapexecute_file_files (subj))
        (call .weechat.exec.read_file_files (subj))
    
        ;; nearly full access to our own data files
        (call .weechat.data.search_file_dirs (subj))
        (call .weechat.data.create_file_dirs (subj))
        (call .weechat.data.create_file_files (subj))
        (call .weechat.data.delete_file_files (subj))
        (call .weechat.data.readwrite_file_files (subj))
        (call .weechat.data.rename_file_files (subj))
        (call .weechat.data.addname_file_dirs (subj))
        (call .weechat.data.deletename_file_dirs (subj))
        (call .weechat.data.rename_file_dirs (subj))
    
        ;; same as above, nearly full access to our runtime files
        (call .weechat.run.search_file_dirs (subj))
        (call .weechat.run.create_file_dirs (subj))
        (call .weechat.run.create_file_files (subj))
        (call .weechat.run.delete_file_files (subj))
        (call .weechat.run.readwrite_file_files (subj))
        (call .weechat.run.rename_file_files (subj))
        (call .weechat.run.addname_file_dirs (subj))
        (call .weechat.run.deletename_file_dirs (subj))
        (call .weechat.run.rename_file_dirs (subj))
    
        ;; allow using unix sockets so long as they are the same type as ourself
        (allow subj self (unix_dgram_socket (create sendto read write)))
    
        ;; allowing using the network but only irc ports specifically
        (allow subj self create_tcp_socket)
        (call irc.nameconnect_port_tcp_sockets (subj))
    
        ;; You need to be able to traverse directories before you can access files.
        ;; Each parent dir needs to be traversable, so we have to allow traversing root.
        (call .root.search_file_dirs (subj))
    
        ;; allow access to procfs
        (call .proc.read_fs_lnk_files (subj))
        (call .proc.search_fs_dirs (subj))
    
        ;; allow access to sysfs
        (call .sys.search_fs_dirs (subj))
        (call .sys.read_fs_files (subj))
    
        ;; use system libraries
        (call .lib.search_file_dirs (subj))
        (call .lib.read_file_files (subj))
        (call .lib.mapexecute_file_files (subj))
        (call .lib.read_file_lnk_files (subj))    
    
        ;; read /etc
        (call .conf.search_file_dirs (subj))
        (call .conf.read_file_files (subj))
        (call .conf.read_file_lnk_files (subj))
        ;; The dynamic loader is currently labeled .conf.file, and we need to be able to map and exec it.
        ;; This is something you probably want to fix when writing your own policy on top of dssp5.
        (call .conf.mapexecute_file_files (subj))
    
        ;; use ssl certs
        (call .cert.search_file_dirs (subj))
        (call .cert.read_file_files (subj))
    
        ;; use terminal
        (call .sys.use_subj_fds (subj))
        (call .dev.readwriteinherited_file_chr_files (subj))
        (call .ptytermdev.readwriteinherited_all_chr_files (subj))    
    
        ;; read /usr/share
        (call .data.search_file_dirs (subj))
        (call .data.read_file_files (subj))
    
        ;; traverse /home
        (call .home.search_file_dirs (subj))
    
        ;; allow creating dirs in ~/.config
        (call .xdg.config.home.search_file_dirs (subj))
        (call .xdg.config.home.create_file_dirs (subj))
        (call .xdg.config.home.addname_file_dirs (subj))
    
        ;; allow creating dirs in ~/.cache
        (call .xdg.cache.home.search_file_dirs (subj))
        (call .xdg.cache.home.create_file_dirs (subj))
        (call .xdg.cache.home.addname_file_dirs (subj))
    
        ;; allow creating dirs in ~/.local/share
        (call .xdg.share.home.search_file_dirs (subj))
        (call .xdg.share.home.create_file_dirs (subj))
        (call .xdg.share.home.addname_file_dirs (subj))
    
        ;; allow creating dirs in ~/.local/state    
        (call .xdg.state.home.search_file_dirs (subj))
        (call .xdg.state.home.create_file_dirs (subj))
        (call .xdg.state.home.addname_file_dirs (subj))
    
        ;; allow creating files in the runtime directory
        (call .run.search_file_dirs (subj))
        (call .runuser.search_file_dirs (subj))
        (call .runuser.create_file_dirs (subj))
        (call .runuser.addname_file_dirs (subj))
    
        (block exec
            (blockinherit .file.exec.template)
    
            ;; Label the weechat executable itself.
            ;; This along with some macros we called earlier cause executing weechat to transition to
            ;; the weechat.subj context.
            (filecon "/usr/bin/weechat" file file_context))
    
        (block data
            ;; This macro will be called at some point and is what makes the files and directories
            ;; weechat creates in ~/.config and such transition to .weechat.data.file type from
            ;; .home.file.
            (macro xdg_file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                (call .xdg.config.home.file_type_transition (ARG1 file ARG2 ARG3))
                (call .xdg.cache.home.file_type_transition (ARG1 file ARG2 ARG3))
                (call .xdg.share.home.file_type_transition (ARG1 file ARG2 ARG3))
                (call .xdg.state.home.file_type_transition (ARG1 file ARG2 ARG3)))
    
            (blockinherit .file.home.template)
    
            (filecon "HOME_DIR/\.config/weechat(/.*)?" any file_context)
            (filecon "HOME_DIR/\.local/share/weechat(/.*)?" any file_context)
            (filecon "HOME_DIR/\.local/state/weechat(/.*)?" any file_context)
            (filecon "HOME_DIR/\.cache/weechat(/.*)?" any file_context))
    
        (block run
            ;; This is similar to the file type transition macro above, but for runtime files instead
            ;; of config and state files.
            (macro file_type_transition_file ((type ARG1) (class ARG2) (name ARG3))
                (call .run.file_type_transition (ARG1 file ARG2 ARG3)))
    
            (blockinherit .file.run.template)
    
            (filecon "/run/user/%{USERID}/weechat" dir file_context)
            (filecon "/run/user/%{USERID}/weechat/.*" any file_context)))
    
    ;; we want files and dirs weechat creates to be of the weechat type so we call our
    ;; type transition macro.
    (call .weechat.data.xdg_file_type_transition_file (.weechat.subj dir "*"))
    (call .weechat.data.xdg_file_type_transition_file (.weechat.subj file "*"))
    
    ;; same as above but for runtime files
    (call .weechat.run.file_type_transition_file (.weechat.subj dir "weechat"))
    (call .weechat.run.file_type_transition_file (.weechat.subj file "*"))
    

    In dssp5 you will notice that we rarely write allow rules directly, we use macros and templates to do the heavy lifting when we can. The templates and macros can be a little confusing at first but they make sense once you start to use them for your modules.

    Selinux is by far the most verbose of the options I listed, but also the most powerful and flexible, and IMO the most fun.

  4. Todo

    For your real policy you want to create abstractions for common behaviour to cut down on the boilerplate.

    A large part of the weechat module could be abstracted out into a new .subj.common module. Common behavor like accessing your own files and accessing things that every process will need like the dynamic loader and system libraries.

    With dssp5 it's up to you to build up abstractions, it only provides a base.

4. Questions

If you have any questions or problems you can email me (my contact info is on my front page), or join the #selinux channel on https://irc.libera.chat.

Author: root

Created: 2025-08-28 Thu 02:50

Validate