Feature Friday #25: Unprivileged execution

Posted by Nick Anderson
August 30, 2024

Generally, cf-agent runs as a privileged user. But did you know that you can also run as an unprivileged user?

A major benefit of running cf-agent unprivileged is the ability to prototype policies during development. However, attempting to execute cf-agent as an unprivileged user without proper configuration will result in errors. Let’s create /tmp/feature-friday-25.cf with the following content:

/tmp/feature-friday-25.cf
bundle agent main
{
  reports: "Happy Friday!";
}

Now, let’s try running that policy with cf-agent as an unprivileged user:

command
cf-agent -Kf /tmp/feature-friday-25.cf
output
   error: cf-promises needs to be installed in /home/nick/.cfagent/bin for pre-validation of full configuration
   error: Failsafe condition triggered. Interactive session detected, skipping failsafe.cf execution.
   error: Error reading CFEngine policy. Exiting...

When cf-agent executes, it wants to use cf-promises from ~/.cfagent/bin, which is empty:

command
ls -al  ~/.cfagent/bin/
output
total 8
drwx------  2 nick nick 4096 Mar  8 09:53 .
drwx------ 11 nick nick 4096 Mar  8 09:53 ..

So that the user’s binaries stay in sync with the package on the system, it’s best to create a symbolic link from ~/.cfagent/bin to /var/cfengine/bin:

command
rm -rf ~/.cfagent/bin && ln -s /var/cfengine/bin ~/.cfagent/bin
ls ~/.cfagent/bin/cf-promises
output
/home/nick/.cfagent/bin/cf-promises

If you run the policy again, you will get an error if the policy file is writable by others:

command
cf-agent -Kf /tmp/feature-friday-25.cf
output
   error: File /tmp/feature-friday-25.cf (owner 1001) is writable by others (security exception)

However, after adjusting the permissions - so that only the owner can write to the file - it runs without any issues:

command
chmod 600 /tmp/feature-friday-25.cf
cf-agent -Kf /tmp/feature-friday-25.cf
output
R: Happy Friday!

Furthermore, seeding the standard library in ~/.cfagent/inputs would be convenient. Let’s use cf-remote to place the standard library for the version of CFEngine that is currently installed:

command
MPF_TARBALL_URL=$(cf-remote --version $(cf-agent --version | head -n 1 | cut -d' ' -f3) list masterfiles | tail -n 1)
MPF_TARBALL_FILENAME=$(basename "$MPF_TARBALL_URL")
TMPDIR=$(mktemp -d --suffix=-seed-mpf-stdlib)
curl "$MPF_TARBALL_URL" --output-dir "$TMPDIR" --output "$MPF_TARBALL_FILENAME" --silent
tar --directory "$HOME/.cfagent/inputs" --gunzip --extract --file $TMPDIR/$MPF_TARBALL_FILENAME ./masterfiles/lib --strip-components 2
rm -rf "$TMPDIR"

Now, we can adjust our sample policy to include the standard library and use something from it:

/tmp/feature-friday-25.cf
body file control
{
  inputs => { "$(sys.libdir)/stdlib.cf" };
}
bundle agent main
{
  reports:

    "Happy Friday!";
    "This policy contains:" printfile => cat( "$(this.promise_filename)" );
}
command
cf-agent -Kf /tmp/feature-friday-25.cf
output
R: Happy Friday!
R: This policy contains:
R: body file control
R: {
R:   inputs => { "$(sys.libdir)/stdlib.cf" };
R: }
R: bundle agent main
R: {
R:   reports:
R:
R:     "Happy Friday!";
R:     "This policy contains:" printfile => cat( "$(this.promise_filename)" );
R: }

However, there are some limitations. Anything that requires elevated privileges will not work since cf-agent won’t elevate privileges to take action. For example, here we adjusted the policy to set the ownership of the policy file to root:

/tmp/feature-friday-25.cf
body file control
{
  inputs => { "$(sys.libdir)/stdlib.cf" };
}
bundle agent main
{
  files:
    "$(this.promise_filename)"
      perms => owner( "root" );

  reports:

    "Happy Friday!";
    "This policy contains:" printfile => cat( "$(this.promise_filename)" );
}

Your OS says “Get out of here!” And CFEngine emits errors.

command
cf-agent -Kf /tmp/feature-friday-25.cf
output
   error: Cannot set ownership on file '/tmp/feature-friday-25.cf'. (chown: Operation not permitted)
   error: Cannot set ownership on file '/tmp/feature-friday-25.cf'. (chown: Operation not permitted)
   error: Errors encountered when actuating files promise '/tmp/feature-friday-25.cf'
R: Happy Friday!
R: This policy contains:
R: body file control
R: {
R:   inputs => { "$(sys.libdir)/stdlib.cf" };
R: }
R: bundle agent main
R: {
R:   files:
R:     "$(this.promise_filename)"
R:       perms => owner( "root" );
R:
R:   reports:
R:
R:     "Happy Friday!";
R:     "This policy contains:" printfile => cat( "$(this.promise_filename)" );
R: }

So, while there is no capability to leverage unprivileged execution to manage resources that require elevated privileges. It’s undoubtedly handy for prototyping policy. And it might save you from some future unintended regrets.

Happy Friday! 🎉