Cfengine 3 Snippets Part 2: sudo

It’s been a while since I’ve really had time to delve too much further into cfengine 3 since my previous post on the subject way back in May but I do have another simple example to share. This time it’s about managing your sudo policy via the sudoers file.

The example is that of a very, very basic sudoers policy but the principles are easily extended to create much more complex policy. The general idea here is that we want cfengine to ensure that specific rules are always in place. Instructed properly, cfengine accomplishes this very well.

Warning: I don’t know anything. I’m just someone learning cfengine 3 and posting about it. If I’m wrong about something, let me know! If you find this at all useful, be my guest. That is all.

################################################################################
##
## FILE: sudo.cf
## DESC: Control /etc/sudoers file on various servers
##
################################################################################

bundle agent sudo
{

vars:

  "sudoers" string => "/etc/sudoers";

  "sudo"     slist => {
                      "%admin ALL = ALL",
                      "%sysadmin ALL = /sbin/mount",
                      "%devel ALL = /sbin/mount"
                      };

packages:

  Night::

  "sudo" -> "Security policy"
    comment               => "Ensure sudo is up to date every 24 hours (and only at night)",
    package_policy        => "update",
    package_method        => yum,
    package_architectures => { "$(sys.arch)" },
    action                => if_elapsed("1440");

files:

  "$(sudoers)" -> "Security Policy"
    comment      => "Append common configuration to sudoers",
    edit_line    => append_if_no_lines("$(sudo)");

}

As with last snippet I posted, the above does not even resemble a complete cfengine policy/configuration, just a small portion that can be contained in it’s own bundle. It can be put in a separate .cf file, imported by promises.cf and added to the bundle sequence, inheriting variables and classes! Also, just like last time I’m using cfengine’s built in interface for package management systems to ensure “sudo” is always installed via yum at night, every 24 hours.

What’s new here is file editing with edit_line and the use of iteration which proves to be very powerful in cfengine 3.

Editing Files

Editing files with cfengine is supposed to be easy but initially it seemed a bit awkward to me.

First you have the promise file promise:

files:

  "$(sudoers)" -> "Security Policy"
    comment      => "Append common configuration to sudoers",
    edit_line    => append_if_no_lines("$(sudo)");

Which makes some reference to the edit_line facility and what looks like a function name with append_if_no_lines(…).

Then you have the edit_line bundle defined elsewhere:

bundle edit_line append_if_no_lines(list)
{
insert_lines:

 "$(list)";
}

Which describes what “append_if_no_lines” actually does.

Of course I’m just learning cfengine and new things can often seem strange and scary but I am finally warming up to editing files with cfengine… I think. What I seemed to have initial trouble with was with the bundles necessary for edit_lines, as described above. The bundle within a bundle concept. The append_if_no_lines and append_if_no_line bundles I’m using are implemented in the cfengine std library, which is highly recommended so that you may avoid re-inventing the wheel a little bit.

For basic promises, to add or remove, comment or uncomments lines and the such there are good edit_lines bundles available in the stdlib. For other more complex or customized file editing, writing your own bundles will be necessary. Either way, understanding what a bundle is and how to create your own is key to fully grasping file editing and getting the most out of it. This seems obvious in retrospect but something I didn’t pickup instantly.

See the cfengine documentation for more about editing files, check the cfengine documentation. There’s waaaaay more good information over there and it’s from the cfengine team, not some random newb.

Iteration

Iteration is powerful mechanism within cfengine that harnesses the power of lists to express a large possible number of actions/operations with very little amount of code. When lists are used, single actions can be made to repeat for every item in a list by using the $(varname) syntax to refer to the list… which as it turns out is the same for scalar values! Funny that!

So cfengine allows us to define X different lines of code to ensure are in a file using only a single file: promise all with the same simple syntax as scalar variables? Brilliant!

A demonstration of iteration can be seen with the $(sudo) slist and the “Append common configuration to sudoers” file: promise. With this single promise definition, up to 6 actual promises are made because the $(sudo) variable is an slist. Each element or item in the list is iterated over in sequence and the promise is evaluated and acted upon, if necessary. The reason that up to 6 promises will be evaluated is the ifvarclass property of promise, ensuring the promise will only be kept if we’re in the context of the class… and looking at the promise to find out which class, we see another example of iteration using the $(sudo) list and the canonify function that turns a string into a class. Thusly, if the host currently running this policy defines all the classes that are tested by the ifvarclass iteration, 6 promises will be made. If the host defines 3 of the classes, then 3 promises will be made and so on, and so forth.

As a beginner, using lists and iteration effectively and creatively seems fairly important to getting the most out of cfengine 3.

Editing Files vs. Copying Files

In my previous snippet, I demonstrated how to promise to copy a file from a secure remote server if the local file does not match the server’s file in order to manage a configuration with cfengine. This time, I’m promising to add lines to a configuration file if they do not already exist exactly as provided.

This represents two rather different takes on policy. The first says: “The configuration must always be exactly like this file, byte per byte!” the second says “These lines must exist but I don’t care about anything else in the file”. The file copy method is what I would call hard policy and the second is soft policy. In the cfengine community solutions, they recommend managing sudo by copying an /etc/sudoers from a remote server. That way is great (just like my DenyHosts example) but this is just another way if you have a use case for cfengine not owning every byte of your configuration file.

Conclusion

Yeah, that’s about it. Enjoy.

3 thoughts to “Cfengine 3 Snippets Part 2: sudo”

  1. Thanks for the comment, Nick!

    I’ve been learning more about cfengine since I originally posted this and the subtlety you describe has become clear to me. I definitely appreciate it being pointed out here for anyone that finds this post and attempts to understand interation based on my description.. Clarity and correctness FTW!

  2. I think there is a small disconnect in whats happening here.
    edit_line => append_if_no_lines(“$(sudo)”);

    Functionally there is no difference between append_if_no_line and append_if_no_lines if you look at them in the standard lib, they both have a single string expansion as the only promiser in insert_lines. They are functionally interchangeable.

    However I believe they express slightly different intensions. Its hinted if you look at the bundles in the stdlib. append_if_no_line takes a str, and append_if_no_lines takes a list. Again, these are just names, nothing enforces that you pass a variable of that type to them, but it makes a difference on how the edits happen.

    You are doing implicit looping over the slist at the files promiser level, so its effectively n file promises where n is the number of list elements you have. When you run your policy you should be able to see this happen where the file is edited multiple times instead of once.

    If you actually pass the list into the append_if_no_lines bundle the implicit lopping happens inside the append_if_no_lines bundle and you only see a single file edit but each line is iterated over and added. Its a subtle difference and there should probably be some comments in the stdlib hinting at that difference since they are functionally interchangeable.

    I posted a gist that shows the difference while running as well as the policy.
    (notice how /tmp/test1 is edited once, and /tmp/test2 is edited 3 times)
    https://gist.github.com/2124224

Leave a Reply

Your email address will not be published. Required fields are marked *