Security Policy and Mandatory Access Control

A system's security policy controls where a process can attach channels in the path space, defines which abilities to assign to its processes, and controls which processes can connect to which others.

Access control is a mechanism used to secure a system by limiting the actions available to a process. Sandboxing, in contrast, constrains code with virtual walls (to protect it from being accessed and damaged). These security measures work well together. A security policy sets out the conditions for access and allows access when they are met.

Mandatory access control (MAC) is policy-driven, with rules to enforce relationships between processes, channels, and paths. For example, rules control which processes can connect to a channel, as well as which specific paths a process may attach to in the path space. It restricts the ability of a process to connect to a channel.

Note: Sandboxing also provides access control but is path-based and depends on the implementation of the resource manager being accessed. Having access denied via channel-based MAC is more secure than sandboxing but is less fine grained.

There is no default security policy. To establish a security policy, you will need:

Policy development involves defining the rules in a text file according to a strict grammar, compiling them for integration into the system, and pushing the compiled policy to procnto to implement them. Once in place, these mandatory access controls are enforced by the process manager rather than at the discretion of individual processes. The enforcement is type based. These rules (in the text file) yield types on individual system processes.

To establish mandatory access controls, you will need:

Type identifiers

A type identifier is a small integer value that is used by QNX Neutrino to define security rules and make policy-based security decisions. It is conceptually similar to a label. All security rules involve one or more types.

Type identifiers currently start at 0 and increase by one for each type declaration. The way they are numbered becomes important only when you use using Qnet and a different security policy between nodes. To ensure the correspondence of type identifiers between nodes, ensure that any types that must be used across nodes are defined in a single file that is compiled into all policies prior to any other type definitions.

In the security policy language, types are represented as names which are then converted to type identifiers in the binary policy file. Type names follow the same rules as C identifiers. They can contain alphanumeric characters and the underscore, but must not start with a digit.

You must declare all types that are used in the policy. Mandatory declaration ensures that mistyped names result in compilation errors instead of unexpected behavior at run time.

Because the type identifiers assigned to types are not fixed, code that uses types should look up the type to convert to an identifier rather than embed a number.

Assigning types to processes and channels

When a process is assigned a type, it acquires:
  • all the procmgr abilities associated with the type
  • the ability to attach its channels to locations in the path space as defined for that type
  • the ability to connect to channels of other processes as appropriate for its type.

The type associated with a process can be set at the time the process is spawned. To change the type identifier, use the on utility with the -T option.

When a channel is assigned a type, the type determines the types that other processes must have in order to connect to it.

The type identifier for a channel is initially set to equal the type identifier of the process that owns it.

Use the PROCMGR_AID_SETTYPEID ability to control the ability of a process to specify a type identifier when a new process is spawned. You can also use the PROCMGR_AID_SETTYPEID ability to indicate that a process is allowed to change its own type. Usually if a process is granted the PROCMGR_AID_SETTYPEID ability, it should be granted only a limited subrange since a process can gain the abilities of any type it is allowed to switch to or spawn processes with.

Note: Processes started from the ifs startup script are type 0 by default, unless explicitly stated otherwise. They have the default set of procmgr abilities (the same as if no security policy is in effect) and are unaffected by abilities specified in the security policy file. They are, however, still restricted in where they can attach channels. Channels of type 0 do not restrict who may connect to them.

A process can change its own type identifier with the procmgr_set_type_id function. When successful, a call to this function changes the security context of the process, including procmgr abilities and the paths the process may attach channels to. The procmgr abilities are determined by the security policy and are unaffected by abilities the process may have had prior to the call.

Note: A process is only able to successfully make the call to procmgr_set_type_id if a security policy has been loaded and if the process currently possesses the procmgr ability PROCMGR_AID_SETTYPEID with a range that covers the type identifier that is being switched to.

Rules for enforcement and syntax

The grammar for the text file supports syntax statements for the keywords type, attribute, allow, allow_attach, and allow_link. They use the following syntax:

type type-name;

Declares a type by name. Type names may contain alphanumeric characters and underscores. They must not start with a number. Use the # character on a new line to comment on the type name. QNX recommends that you do not use hyphens in type names. The type name default is reserved, must be declared only if it is referenced, and is associated with type ID 0. The type name self is reserved, must be declared, and specifies that a process is allowed to connect to itself.

In the following example, screen_t is the type:

# Type to represent the screen process 
type screen_t;
attribute attribute-name;

Declares an attribute by name. Groups a set of types that have some security attributes in common. Attribute names must be different than type names. Use a comma as shown in the following syntax to associate an attribute name with a type name after you declare it:

type type-name [, attribute-name] ... ;

When you specify an attribute for a type, the type can do anything the attribute can do. Types can be associated with multiple attributes. Attributes do not have type IDs.

In this example, the following section of a security policy could be rewritten using an attribute and the reserved type self:

allow secure1_t secure1_t : channel connect;
allow secure2_t secure2_t : channel connect;
allow secure3_t secure3_t : channel connect;

For example:

attribute secure;
type secure1_t, secure;
type secure2_t, secure;
type secure3_t, secure;
allow secure self : channel connect;
allow source-set target-set : class-set permission-set;

Grants procmgr abilities (or the ability to connect to channels) to a type. When a process that is a member of source-setacts on an object whose type is a member of target-set and whose class is any member of class-set, that process has all the permissions of permission-set.

There are two options for class-set:
  • Use ability to bestow procmgr abilities on processes with a given type.
  • Use channel to establish which channel types a process (of a given type) can connect to.

The effect of an allow statement is equivalent to taking all combinations of the source-set, target-set, class-set and permission-set and treating them as if that combination was used in a separate allow statement. The source-set and target-set can be comprised of types, attributes, or any combination of the two.

For example, the following rule uses allow to assign permissions for specific sources and targets:

allow { type1 type2 } { type3 type4 }:channel connect;

The following set of rules creates permissions that are equivalent to the example allow statement:

allow type1 type3:channel connect;
allow type1 type4:channel connect;
allow type2 type3:channel connect;
allow type2 type4:channel connect;

The self type

Use the self type when you need to have the target type in an allow statement match the source type. The type self always refers to the same type as the source type.

For example, suppose you want to allow a process of type server1 to connect to a channel of the same type. If server1 is a type, the following rule allows the type server1 to connect to a channel of type server1

allow server1 self:channel connect;

If instead server1 is an attribute shared by types server_a and server_b, then the statement is equivalent to the following:

allow server_a server_a:channel connect;
allow server_b server_b:channel connect;

Using the channel class to control connections

By default, a process of any type is only allowed to connect to channels that have the type default. But because the type of a channel defaults to the process type, channel types are usually not the default type. Most channels require explicit rules to allow them to be connected to.

The channel class supports the two following permissions:
  • connect — governs a process's ability to connect to a channel on the same node.
  • net_connect — governs the ability for a process from another Qnet node to access a channel on the local one.

The following allow statement allows a process of type mm_client to connect to a channel on the same node that has the type mm_server:

allow mm_client mm_server:channel connect;

The following statement allows this same channel to be connected to by a process of type mm_client that is running on another node:

allow mm_client mm_server:channel net_connect;

Using the ability class to grant procmgr abilities

Use the allow statement with the ability class to grant an ability to a type. You assign permissions using the ability names. For example, to give the type server the abilities that allow it to map physical memory over some range, switch to user IDs 4, 6 and 23, and create dynamic abilities, use the following rule:

allow server self:ability {
    mem_phys:0x1200000-0x24000000
    setuid:4,6,23
    able_create
};

By default, only root gets the abilities that are listed. To grant them to non-root as well, include the nonroot option:

allow server self:ability {
    nonroot
    mem_phys:0x1200000-0x24000000
    setuid:4,6,23
    able_create
};

In this rule, nonroot indicates that the abilities are being granted to both root and non-root. There is no facility to grant an ability to non-root only.

Unlocking abilities and changing inheritability

By default, the abilities are all locked and inherited. This behavior ensures abilities are fixed: if you know the type of a process, then you also know what abilities it has. If abilities are not locked or not inherited, then they are less certain. While not rendering a system insecure, it does make it harder to assess its security.

Unlocked abilities are at least partially under the control of the process. Whether it handles them well or badly is frequently unknown. Abilities that are not inherited frequently result in the loss of root abilities when a child is spawned, but can also result in an increase of abilities since subranges are discarded.

In many cases, abilities are left unlocked and not-inherited to allow the process itself to drop abilities it no longer needs after initialization and avoid having its children inherit root privileges that it required running as non-root.

An alternative to this approach is to provide the process with the type identifier that it should switch to following initialization and one or more type identifiers to spawn its children as. Then the security policy of the system can be configured centrally, outside of the individual processes.

Reasons to leave abilities unlocked or not inherited include the following:

  • If a resource manager uses procmgr_ability() to modify its abilities and the abilities are locked, the call fails (and the resource manager may terminate).
  • If the resource manager is being run as non-root with additional root abilities, then if it is given spawn or fork abilities any child process inherits the additional abilities.

To leave abilities unlocked, use the unlock option. To have them not-inherited, use the noinherit option. For example, the following rule grants the same abilities as the previous example but leaves them unlocked and not inherited:

allow server self:ability {
    noinherit
    unlock
    mem_phys:0x1200000-0x24000000
    setuid:4,6,23
    able_create
};
Note: If any rule unlocks a particular ability it remains unlocked irrespective of any rules that may follow.

Custom abilities

In some cases, a process needs a custom ability (sometimes called dynamic abilities). For example, if server also needs to be able to bind to a privileged TCP port:

ability network/bind/privport;
 
allow server self:ability {
    network/bind/privport
    mem_phys:0x1200000-0x24000000
    setuid:4,6,23
    able_create
};
Note: Custom abilities must be declared.

Ability ranges

As shown in some of the previous examples, when a type is granted an ability, the specification can include a range. You can specify a single range or a comma-separated list of ranges. For the majority of abilities, ranges are numeric and consist of a range start and end value separated by a hyphen. The start and end are represented in decimal, octal, or hexadecimal with conventional notation.

If only the start of range is specified with no hyphen, the start and end values are the same. If the end is omitted but the hyphen is still present, the end of the range is equal to the largest possible value.

A range for the settypeid and channel_connect abilities is a single type name because these abilities both reference types whose numeric value can change:

allow server self:ability {
    # Grant mem_phys over the range 1024 to 4096 and 
0x1200000 to 0x24000000
    mem_phys:1024-4096,0x1200000-0x24000000
    # Grant setuid for 4, 5, 6, 23 and 96 and above
    setuid:4-6,23,96-
    # Grant settypeid for the type ids associated with types 
server1 and server2
    settypeid:server1,server2
    # Grant channel_connect for type server3
    channel_connect:server3
    # Allow a switch to a type that has the pathspace ability
    gain_priv:pathspace
};

The ability name and any ranges may not have any space separating them. It is not necessary to specify all ranges for a given ability in a single rule. If multiple rules specify ranges for the same ability, the individual ranges are combined.

The default_priv, root_priv, and nonroot_priv pseudo-abilities

There are also several pseudo-abilities that affect which abilities a type is granted. Although pseudo-abilities make it easy to grant large sets of abilities to processes, which might makes it easier to get a system working, it also might leave the system considerably less secure. Use them with caution.

The default_priv pseudo-ability gives a type default behavior for all abilities not explicitly granted for the type. It changes the special PROCMGR_AID_EOL ability that terminates ability lists. Without this ability, the PROCMGR_AID_EOL ability includes the options to deny and lock all further abilities for both root and non-root. When default_priv is granted, no additional flags are passed with the PROCMGR_AID_EOL ability, which leaves other abilities in their default state.

The two pseudo-abilities root_priv and nonroot_priv represent the set of static abilities given by default to root and non-root processes respectively. The following two statements are equivalent:

allow t1 self:ability {
    root_priv
};
allow t1 self:ability {
    spawn_setuid spawn_setgid setuid setgid getid pathspace 
reboot cpumode runstate confset rsrcdbmgr session
    umask event rlimit mem_add mem_phys mem_special 
mem_global mem_peer mem_lock wait v86 qnet clockset 
clockperiod
    interrupt keydata io trace priority connection schedule 
signal timer path_trust swap child_newapp aps_root
    able_create default_timer_tolerance xprocess_query chroot 
power srandom sandbox qvm rlimit_peer mac_policy settypeid
};

The former has the advantage because it expresses the intent more clearly. Using root_priv does not necessarily grant the abilities to root nor does nonroot_priv grant the abilities to non-root, they are simply the equivalent to listing the abilities explicitly.

For example, the following statement gives all the abilities normally granted to both root and non-root to both root and non-root users:

allow t1 self:ability {
    nonroot
    root_priv
    nonroot_priv
};

The default_priv, root_priv and nonroot_priv are broad in scope and provide an excessive set of abilities, but you can use a minus sign (-) to specify abilities to exclude. For example, the following statement defines a set of abilities that includes all root abilities except for mem_phys and keydata:

allow t1 self:ability {
    root_priv
    -mem_phys
    -keydata
};

When this exclusion syntax is used with default_priv, the type gains default treatment for all abilities it has not explicitly been granted with the exception of the ones excluded.

Note: The minus sign does not necessarily deny the ability, though it might have this effect. The effect of negation is limited to the statement in which it is used.

In the previous example, the statement did not grant the keydata ability to the type because keydata is excluded but it is entirely possible that statements before or after might have given keydata.

For example, the following statements represent a complicated way of giving all root privileges to a type:

allow t1 self:ability {
    mem_phys
};
allow t1 self:ability {
    root_priv
    -mem_phys
    -keydata
};
allow t1 self:ability {
    keydata
};
allow_attach source-set path-set [new-type];

Creates a rule to let a process attach one of its own channels to a path. If the channel successfully attaches to the path, the channel optionally becomes the new type. Paths can include wildcards and ellipses.

In this example, the rule allows io-pkt to attach to many paths under /dev/socket:

allow_attach io_pkt_t /dev/socket/* socket_t;

In this next example, the rule allows unrestricted_t to attach a channel to any path:

allow_attach unrestricted_t /…;
allow_link source-set path-set;

Creates a rule to let a process create a procmgr symbolic link or attach the channel of another process to a path. It affects the ability of a process to use the functions pathmgr_symlink and pathmgr_link (when the channel involved belongs to another process). The source of the link can be a string or a combination of node, process, and channel identifiers. Paths can include wildcards and ellipses.

In this example, the rule allows process of type type1_t to replace the dynamic linker by pointing it to somewhere other than /proc/boot/libc.so.4:

allow_link type1_t /usr/lib/ldqnx-64.so.2;

Summary of policy rules

Rule: Description:
allow Gives procmgr abilities or the ability to connect to channels to a type or attribute, or a set if you use {}.
allow_attach Allows the resmgr_attach() of a name.
allow_link Allows the pathmgr_symlink() of a name.
ability Declares a list of abilities to assign to a type or attribute.

Summary of special types in a policy

Rule: Description:
default You must declare this type to use it (but you do not have to use it). Any process can connect to a channel of this type.
self The reflexive type. When you use it as a target-set, it refers to the individual source types.
default_rules The rules given to all types by default. The set of non-root procmgr abilities, unless you redefine this type (by declaring it and setting it differently).

Summary of special permissions for abilities in a policy

Rule: Description:
nonroot Allows root-privilege ability to a non-root process.
root_priv The default set of abilities for a root process.
nonroot_priv The default set of abilities for a non-root process.
noinherit The abilities that are not inherited when a process is created.
unlock The process may change or drop abilities while running.
gain_priv A type change may allow an increase in privilege.
Note: To remove an ability from a previous list, start a line with the - sign. Use :range-range,range-range (with no spaces) for subranges.

Defining your policy

QNX recommends that the MAC security policy be developed independently by a person who is dedicated to security. The rules in this policy should take into consideration the intended level of system security and be used in conjunction with other security measures such as best practices. The person who develops the policy does not need to be the same person who is responsible for integrating it into the system.

It can take a while to develop a security policy; it takes time to determine which abilities each specific process needs. Policy development is a scenario-dependent activity that requires trying different things with the system until you get the policy to where you want it to be.

You can use the secpolmonitor utility to collect the following information:

The secpolmonitor utility is a verbose utility that also provides a way for you to determine when operations fail due to an installed security policy, usually because of an error in the policy.

Example of an uncompiled policy file (secpol.txt)

The following example illustrates a way to use security policy to control the use of the path /dev/screen as is used by the screen service and its clients:
# Type to represent the screen process
type screen_t;
# Type to represent screen clients (you might want more than one type)
type screen_client_t;
        
allow_attach screen_t /dev/screen;
allow screen_client_t screen_t : channel connect;
        
This policy introduces two types named screen_t and screen_client_t, one to be used when starting the screen service, one to be used when starting its clients. The allow_attach rule states that any process labeled with screen_t is allowed to attach a channel to the path /dev/screen. Once the policy has been installed in procnto, it will not be possible to attach any path that is not explicitly allowed including procmgr symbolic links.

The example shows the simpler form of the allow_attach statement where there is no change in the type associated with the channel being attached. Channels inherit the type of the process that owns them; this means that this channel will also have a type of screen_t. By default, no process is able to connect to a channel with type screen_t. To make the channel useful, an allow statement is added to indicate that and a process with the type screen_client_t is allowed to connect to a channel that has a type screen_t.

Working without a policy in place

If you chose not to push a policy to procnto (whether or not you have defined one), the type ID for all channels remains 0 and the ability of a process to attach to a path relies solely on the procmgr ability PROCMGR_AID_PATHSPACE.

Without a policy in place, channels retain their default type ID of 0 (since their process type ID is 0) and processes are unrestricted in their ability to connect to a channel with a type ID of 0.

Compiling and implementing the policy

A policy can be mutable or immutable, depending on how you compile it. For more information, see the secpolcompile and secpol utilities.

Enforcing the policy

Once the policy is pushed in place, channel-based mandatory access controls are enforced by the operating system.

Developing a policy

The goal of writing a security policy is to be able to run processes on a system with only the capabilities they need, to minimize the potential damage that could occur should one of them be compromised. Consider that the required capabilities for many resource managers are not always fixed; for example, additional abilities may be required only during initialization. Another thing to think about is when a process spawns another one. The child process might have different security requirements from its parent process.

Trying to develop a policy from scratch, based solely on known information about interrelationships between processes and knowledge of the abilities they require, is difficult. The simplest approach is to run a system as it is intended to be run, have the secpolmonitor utility monitor all security events, and then you can generate a policy from its output. Running a system as it is meant to be run means starting all processes with their intended uids and types.

Taking this approach lets you directly use the secpolmonitor output in your policy development. For example, if it says that type io_pkt_t needs ability CONFSET:19, then you know that this type requires it (whether or not the io-pkt process itself, or a process it spawns, uses this ability).

Suppose, in the final system, it is intended that io-pkt be run as uid 21, gid 21, and type io_pkt_t. In the startup script for the system, you might want something like this line:
on -T io_pkt_t -u 21:21 io-pkt-v6-hc -de1000 -p qnet -p tcpip random
However, without a policy in place this won't work; since procnto does not know about type io_pkt_t, it refuses to use it. If you remove this option, then io-pkt fails to run because it is missing abilities. A policy needs to be loaded, but we haven't got one yet.

Write a policy that allows everything for all types, without restriction. Under this policy, the system is completely insecure and runs normally. Since everything is being run with the proper types and proper uids, the output of secpolmonitor should accurately reflect what's needed.

An example of a policy that can be used for policy generation follows. This policy defines four types though other types are easily added; each just needs to have the attribute superpowers to allow it do everything.

attribute superpowers;
 
type default_rules;
allow default_rules self:ability {};
 
type io_pkt_t, superpowers;
type sshd_t, superpowers;
type devb_t, superpowers;
type random_t, superpowers;
 
ability iofunc/dup;
ability iofunc/exec;
ability iofunc/chown;
ability iofunc/read;
ability vfs/mount-blk;
ability network/bind/privport;
ability network/interface/setpriv;
 
allow superpowers self:ability {
   nonroot
   default_priv
   root_priv
   nonroot_priv
   channel_connect
   iofunc/dup
   iofunc/chown
   iofunc/read
   iofunc/exec
   vfs/mount-blk
   network/interface/setpriv
   network/bind/privport
   unlock
};
Before running anything, you want to run the secpolmonitor utility with a command line such as:
secpolmonitor -pasno /tmp/secpol.out & 

This command captures all path attachments, as well as uses of abilities, and records them to the file /tmp/secpol.out. With a policy similar to this one, the system is likely to run as it should. However, if it fails, the most likely explanation is that there are processes that create additional dynamic abilities and a process fails because it has not been granted them. For more information, look at error lines in secpol.out. If you find some, change the policy so that the ability names are declared in the policy file and given to the superpowers attribute.

By default, secpolmonitor treats all dynamic abilities as if they don't support subranges (since most don't and it has no means to know otherwise). If you have a dynamic ability that uses subranges and you want it captured in the policy, adjust the secpolmonitor command to something such as:

secpolmonitor -pasno /tmp/secpol.out -S subrangedAble &

To make the final policy as complete as possible, try and exercise as much of the system as you can while you are developing the policy. If anything is missed in this exercise, then the resulting policy may be missing rules. It is also important to try to avoid doing things that should not be allowed. For example, if the system allows a user to log into a non-privileged account, then while the system is running this policy, the user is able to do anything (such as slay system services). If this happens while the rules are being generated, then the final rules will allow this behavior even though it is not intended. Missing rules can be corrected after the fact by having secpolmonitor running to detect errors.

To find rules that are unintended, look at the resulting policy file. After the system has been exercised sufficiently, the output file can be copied off the system and used as the basis of the real security policy. The simplest way to do this is to use a program to process the output. An example of such a program is genpol, a Python script that generates a security policy from secpolmonitor output.

One difficulty with automatic rule generation is handling processes that don't handle policies imposed upon them as expected. Processes that try to control their own abilities, for example, expecting to be started as root and then dropping abilities on their own, frequently behave differently than expected if they are constrained by a policy. Their attempt to drop abilities by calling procmgr_ability is liable to fail since policies generated by secpolmonitor typically have abilities locked.

Give the default_priv ability to types that are used to running this service, and make sure that the default_rules type has no abilities. The genpol script automatically does this for a number of services; add new ones by updating the problem_cases variable at the top of this script. Services likely to cause problems frequently show problems when you try to run them using the policy that results from this process. Alternatively, if you search for and find the string procmgr_ability in the binary, it is likely going to have problems.

Summary of keywords and rules

Category: Syntax:
Types

type type-name;

The reserved type names are:

  • self
  • default
  • default_rules
Attributes attribute attribute-name;
Allow

allow source-set target-set : class-set permission-set;

The class-set contains:

  • channel
  • ability

The options for channel are:

  • connect (most common)
  • net_connect (over Qnet)

The options for ability are:

  • procmgr abilities which may include ranges
  • dynamic abilities
  • unlock
  • noinherit
  • pseudo-abilities

The pseudo-abilities are:

  • default_priv
  • gain_priv
  • root_priv
  • attach
  • settypeid
Allow attach allow_attach source-set path-set [new-type];
Allow link allow_link source-set path-set;