Change in behavior: Directory permissions and the execute bit

Posted by Nick Anderson
March 29, 2022

rxdirs has provided a convenient default when setting permissions recursively. When enabled (the default prior to version 3.20.0) a promise to grant read access on a directory is extended to also include execution since quite commonly if you want to read a directory you also want to be able to list the files in the directory. However, the convenience comes with the cost of complicating security reviews since the state requested on the surface is more strict than what is actually granted. This can both undermine the understanding of the desired state of the permissions as well as confidence that the policy accurately describes the resulting state and we have decided the convenience is not worth the cost.

In the upcoming release of CFEngine 3.20.0 the default setting for rxdirs is changing from true to false. In this post we discuss changes you may want to make to your policy as a result of the change.

For example, here we promise that both a file and a directory should exist and be readable by the owner (mode 0400) while rxdirs is enabled:

bundle agent __main__
{
  files:
      # A directory promises to be readable, with rxdirs enabled it's also executable
      "/tmp/a-directory/."
        create => "true",
        perms => m_rxdirs( "0400", "true" );

      # A file promises to be readable, with rxdirs enabled it's just readable, not executable
      "/tmp/a-file.txt"
        create => "true",
        perms => m_rxdirs( "0400", "true" );

  reports:
      # Show human readable and octal permissions of each file
      "stat -c '%A %a' /tmp/a-directory: $(with)"
        with => concat( filestat( "/tmp/a-directory", "permstr" ),
                        " ",
                        filestat( "/tmp/a-directory", "permoct" ));

       "stat -c '%A %a' /tmp/a-file.txt: $(with)"
        with => concat( filestat( "/tmp/a-file.txt", "permstr" ),
                        " ",
                        filestat( "/tmp/a-file.txt", "permoct" ));


}

body perms m_rxdirs( mode, rxdirs )
{
  mode => "$(mode)";
  rxdirs => "$(rxdirs)";
}

Running the policy we can see that the directory but not the file gets the additional permission of execute:

# cf-agent --no-lock --log-level info --file /tmp/rxdirs-illustrated.cf
    info: Created directory '/tmp/a-directory/.'
    info: Object '/tmp/a-directory' had permissions 0755, changed it to 0500
    info: Created file '/tmp/a-file.txt', mode 0400
R: stat -c '%A %a' /tmp/a-directory: dr-x------ 500
R: stat -c '%A %a' /tmp/a-file.txt: -r-------- 400

With the change in default value directories will no longer get the additional permissions unless rxdirs is explicitly enabled. Furthermore, beginning with binary version 3.18.2 or greater Warnings will be emitted when a perms body is used setting mode without explicitly having set rxdirs.

For example, warning: Using the default value 'true' for attribute rxdirs (promiser: /tmp/rxdirs/.), please set it explicitly is already emitted by 3.18 nightly packages for the following policy snippet.

bundle agent main
# User Defined Service Catalogue
{
  files:
    "/tmp/rxdirs/."
      create => "true",
      perms => mode( "0400" );
}

body perms mode( perm_or_oct_string )
# @brief Set the file mode
# @param perm_of_oct_string The new mode, e.g. 0400, o+rx-w, go-rwx
{
      mode   => "$(perm_or_oct_string)";
}

The MPF (Masterfiles Policy Framework) has been instrumented to maintain the old rxdirs default value expectation until you are running binaries version 3.20.0 or later meaning you can safely run the newer policy with older binaries and expect to have the same behavior until your binaries have been upgraded to 3.20.0 or newer. If you upgrade binaries to 3.18.2 or later before upgrading the policy framework you can expect to see many warnings as illustrated by the following output (which has been uniquified for brevity) from a host running update.cf with an out-dated policy framework and newer binaries

 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/masterfiles/., file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:283), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:777), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/apk, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/apt_get, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/freebsd_ports, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/msiexec.bat, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/msiexec-list.vbs, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/nimclient, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/pkg, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/pkgsrc, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/slackpkg, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/snap, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/WiRunSQL.vbs, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/yum, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly
 warning: Using the default value 'false' for attribute rxdirs (promiser: /var/cfengine/modules/packages/zypper, file: /var/cfengine/inputs/cfe_internal/update/update_policy.cf:763), please set it explicitly

What should you do?

Upgrade your policy with builds from master or 3.18.x and address any Warnings.

Tip: Getting nightly packages for 3.18.x is quick and easy with cf-remote, cf-remote --version 3.18.x list

Review your policy for cases where perms bodies affect mode to see if they are applied to directories and adjust as necessary.

Typical treatment will be to use a perms body with rxdirs explicitly enabled or to make one promise that affects non directories and another promise that affects directories.

To make a promise that applies recursively to files but not directories use a file_select body. For example, here file_types specifies dir and file_result specifies !file_types, thus selecting all files that are not of type dir. Note: body file_select not_dir was recently added to the standard library and will be present in 3.18.2 and later releases.

body file_select not_dir
# @brief Select all files that are not directories
{
      file_types => { "dir" };
      file_result => "!file_types";
}

For example, this policy illustrates how to make promises including the execute bit for just directories and promises without the executable bit for files. The commented out promise being the equivlent prior to the change in rxdirs default.

bundle agent init
{
  vars:
      "init_files"
        slist => {
          "/tmp/rxdirs/A/file.txt",
          "/tmp/rxdirs/A/directory/file.txt",
          "/tmp/rxdirs/A/directory/promised/file.txt",
          "/tmp/rxdirs/B/file.txt",
          "/tmp/rxdirs/B/directory/file.txt",
          "/tmp/rxdirs/B/directory/promised/file.txt"
        };

  files:
      "$(init_files)"
        create => "true";
}

bundle agent example_rxdirs_treatment
{
  files:

      # A promise that would have resulted in owner getting execute on
      # directories prior to the rxdirs behavior change.
      #
      # "/tmp/rxdirs/."
      #   depth_search => recurse_with_base( "inf" ),
      #   perms => m( "0400" );


      # Adjusted implementation to promise permissions and files separately
      "/tmp/rxdirs/A/."
        file_select => dirs, # Select directories only
        depth_search => recurse_with_base( "inf" ),
        perms => m( "0500" );

      "/tmp/rxdirs/A/."
        file_select => not_dir, # Select anything that isn't a directory
        depth_search => recurse_with_base( "inf" ),
        perms => m( "0400" );

      # Alternative implementation, using rxdirs
      "/tmp/rxdirs/B/."
        depth_search => recurse_with_base( "inf" ),
        perms => m_rxdirs( "0400" ); # Define a custom perms body with rxdirs enabled
}

bundle agent __main__
{
  methods:
      "Initialize file tree for illustrating rxdirs behavior"
        usebundle => init;

      "Illustrate implemntation options for dealing with change in rxdirs behavior"
        usebundle => example_rxdirs_treatment;
}

body perms m_rxdirs( mode )
# @brief Custom body with behavior of body perms m from standard library with rxdirs enabled
{
        inherit_from => default:m( "$(mode)" );
        rxdirs => "true";
}

# Plucked from lib/files.cf #
#
# From here down are bodies defined in the standard library, duplicated here for
# convenience of running a standalone example policy

body perms m(mode)
# @brief Set the file mode
# @param mode The new mode
{
        mode   => "$(mode)";

      #+begin_ENT-951
      # Remove after 3.20 is not supported
        rxdirs => "true";
@if minimum_version(3.20)
        rxdirs => "false";
@endif
      #+end
}

body depth_search recurse_with_base(d)
# @brief Search files and directories recursively up to the specified
# depth, starting from the base directory excluding directories on
# other devices.
#
# @param d The maximum search depth
{
        depth => "$(d)";
        xdev  => "true";
        include_basedir => "true";
}

body file_select dirs
# @brief Select directories
{
        file_types  => { "dir" };
        file_result => "file_types";
}

body file_select not_dir
# @brief Select all files that are not directories
{
        file_types => { "dir" };
        file_result => "!file_types";
}

In the output we can see that the resulting permissions are the same between /tmp/rxdirs/A and /tmp/rxdirs/B with the permissions in /tmp/rxdirs/A set by independent promises for files and directories and the permission under /tmp/rxdirs/B set with rxdirs enabled.

# cf-agent --no-lock --log-level info --file /tmp/rxdirs-inherit-from-same-name.cf
    info: Created directory for '/tmp/rxdirs/A/file.txt'
    info: Created file '/tmp/rxdirs/A/file.txt', mode 0600
    info: Created directory for '/tmp/rxdirs/A/directory/file.txt'
    info: Created file '/tmp/rxdirs/A/directory/file.txt', mode 0600
    info: Created directory for '/tmp/rxdirs/A/directory/promised/file.txt'
    info: Created file '/tmp/rxdirs/A/directory/promised/file.txt', mode 0600
    info: Created directory for '/tmp/rxdirs/B/file.txt'
    info: Created file '/tmp/rxdirs/B/file.txt', mode 0600
    info: Created directory for '/tmp/rxdirs/B/directory/file.txt'
    info: Created file '/tmp/rxdirs/B/directory/file.txt', mode 0600
    info: Created directory for '/tmp/rxdirs/B/directory/promised/file.txt'
    info: Created file '/tmp/rxdirs/B/directory/promised/file.txt', mode 0600
    info: Object '/tmp/rxdirs/A/./directory/promised' had permissions 0755, changed it to 0500
    info: Object '/tmp/rxdirs/A/./directory' had permissions 0755, changed it to 0500
    info: Object '/tmp/rxdirs/A/.' had permissions 0755, changed it to 0500
    info: Object '/tmp/rxdirs/A/./file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/A/./directory/file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/A/./directory/promised/file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/B/./file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/B/./directory/file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/B/./directory/promised/file.txt' had permissions 0600, changed it to 0400
    info: Object '/tmp/rxdirs/B/./directory/promised' had permissions 0755, changed it to 0500
    info: Object '/tmp/rxdirs/B/./directory' had permissions 0755, changed it to 0500
    info: Object '/tmp/rxdirs/B/.' had permissions 0755, changed it to 0500
Get in touch with us
to discuss how we can help!
Contact us
Sign up for
our newsletter
By signing up, you agree to your email address being stored and used to receive newsletters about CFEngine. We use tracking in our newsletter emails to improve our marketing content.