Using group membership in Active Directory to determine Hiera used for lookups

Before you join your Linux machine to Active Directory, you usually create (or “pre-stage”) a Computer Object. A Computer Object is really pretty much the same as a User Object. It has a password, it has a principal name and for the purposes of this article, it has group membership. What if we could use that group membership to determine which Hiera files to do lookups from? Let’s take a crack at it.

Imagine you have the following Hiera data layout:

    domain computers.yaml
    ncsu-linux computers.yaml
    coedean-lab machines.yaml

What we expect is that there will be configuration we wish to apply if a machine is in “ncsu-linux computers” and/or “coedean-lab machines”. Inside those Hiera files might be anything under the sun. It doesn’t matter. However, if a machine is in any of those groups, then those files will be scanned when Hiera lookups are performed.

As you probably know, there isn’t a built-in fact for determining a host’s group membership in Active Directory. Fire up your text-editor of choice and we’ll create one!

Creating the fact

Choose (or create) a module for this fact to live in. You’ll need to create the following file structure from the root of your module:


The bare-minimum for any fact is to register the fact with Facter using Facter#add and to implement the “setcode” method. Whatever is returned from that method becomes the value of the fact. Add the following to groups.rb:

Facter.add(:groups) do
  setcode do
    'Hello, World!'

So, if you committed this and ran Puppet, you would see a new fact named “groups” reported and the value would be “Hello, World!”. Not very useful, yet. Before we move on, let’s figure out how to determine which groups a machine a member of in Active Directory.

Determining group membership

Remember that the Computer Object you create in Active Directory is not much different from a User Object. If we were doing a user lookup on any standard Linux distribution, we might use the id-command. Let’s try that with our Computer Object name then:

[mdwheele@crab-battle ~]$ id blaze
id: blaze: no such user

Hrm… no user called “blaze”. But I named it “BLAZE” when I created the Computer Object! Let’s try something else.

When a Linux machine is joined to Active Directory, a Kerberos keytab is created for the machine at “/etc/krb5.keytab” (by default). A keytab is effectively a store for one or more principal keys. In this case, it will contain the Computer Object’s password in Active Directory. This is not a password that can be used to authenticate to the machine. It is the password that the Computer uses to authenticate itself to Active Directory. So what’s in this keytab? Run “klist -k” as root:

[root@crab-battle ~]# klist -k
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------

There’s a dollar-sign… why is there a dollar-sign? Let’s give it a shot:

[mdwheele@crab-battle ~]$ id blaze$
uid=1000713862(blaze$) gid=1000000515(domain computers) 
groups=1000000515(domain computers),1000701942(ncsu-linux computers),
1000201248(ncsu-read group memberships),1000000000

The dollar-sign is a convention used to distinguish Computer Accounts from User Accounts in Active Directory. It is the NETBIOS name, appended with a “$”, to form the “sAMAccountName” for the computer. Finally, we have our list of groups! Now let’s get that into our fact.

require 'socket'

Facter.add(:groups) do
  setcode do
    # Get the computer account from FQDN (e.g. "blaze$" if
    host_principal_name = Socket.gethostname[/^[^.]+/] + '$'

    # This is the command to do lookup
    cmd = sprintf('id %s', host_principal_name)

    # Start with empty array of groups
    groups =

    # Only run on machines joined to active directory.
    res = Facter::Core::Execution.execute('realm list | grep active-directory', 
      :on_fail => nil)

    if res != nil then
      # Run the command and use match to grab list of groups.
      groups_raw = Facter::Core::Execution.execute(cmd)

      # If has groups, use a RegExp to strip out all group names.
      if groups_raw != nil then
        groups = groups_raw.scan(/\(([^)]+)\)/).flatten

    # Return the list of groups we compiled.

There’s a lot going on in our fact, but the gist of it is that we use the output of “realm list” to determine whether or not the machine is joined to an Active Directory. If so, we call “id computer-name$” to do a lookup and scrape group membership out of that. Finally, we pack all of that up into an array and return from the fact. If you commit this and run Puppet, you should see the value for “groups” be updated to become an array of groups.

Note: For folks at NC State, it is completely likely that you will inherit a Computer Object that was a Windows machine in a previous life. Because of this, it may be member to groups you don’t expect. Keep that in mind.

So how does this fact get mapped back to Hiera? Wouldn’t I need to create specific levels in my hierarchy for each group because Hiera only does one pass over levels? How do I handle my new, shiny groups fact that has an array value?

Wrapping up with Hiera’s “Mapped paths”

When you’re specifying hierarchy levels in your Hiera configuration, you have to give each a name and some other options. One of those options is “where to find yaml data files”. This can be specified via “path”, “paths”, “glob”, “globs” or “mapped_paths”). If you use “mapped_paths”, you can specify a fact that is a collection of values (array or hash). When Hiera does lookups, it will expand this value to produce an array of paths. Here’s what our “Active Directory Group” layer might look like:

- name: Active Directory Group
  mapped_paths: [groups, group, "groups/%{group}.yaml"]

This will take “groups” and expand it to “groups/ncsu-linux computers.yaml”, “groups/domain computers.yaml” and “groups/coedean-lab machines.yaml” (if it were a member of all three).

Just like normal Hiera lookups, sometimes these files won’t exist and that’s okay. But if they do exist, they will be used as part of the lookup for a particular key and merged according to lookup specification. In this way, you can use group membership in Active Directory as a tool to specify additional configuration to your machines via Hiera.