Admin Account Schema Extensions for AD

The best administration models for Active Directory prescribe some level of account segregation - at least for any user holding sensitive group memberships like Domain Admins - and in some cases might even require multiple admin accounts per individual administrator, such as with Microsoft's tiered administrative model. But how might one keep track of these account relationship, for lifecycle management purposes for example...? Today, let's have a look at an extension to the Active Directory schema that might help!

AD Account Segregation 101

The built-in administration model in Active Directory is dangerously permissive towards behaviors that expose Domain Admins and other sensitive accounts to credential theft in most enterprise networks of today - like, allowing logons to workstations and other client member computers which might have been compromised already, by default. This leads the uninitiated to employ all sorts of inappropriate points of contact between highly privileged accounts and untrusted assets.

For this reason, one of the most fundamental steps you can (and should) take to secure your AD environment is to establish some form of account segration.

SwiftOnSecurity_tweet_1170349787293655040-1

It can be as simple just giving your admins an extra account to which administrative privileges can be assigned. This admin account, in turn, should not be used for day-to-day tasks like reading email or browsing the internet. This paradigm is prevalent in (and sometimes a good fit for) smaller operations where helpdesk/sysadmin responsibilities are shared by the same employees.

Lockdown mode!

On the other end of the spectrum, we find Microsoft's Active Directory administrative tier model

paw_rm_fig1-1-

In it's most extreme form, this 3-tier model is enforced via a dedicated and appropriately hardened administrative forest (colloquially referred to as a "Red Forest" or an ESEA - Enhanced Security Administrative Environment), but it can also be implemented directly in a single forest to provide appropriate segregation not just between servers and workstations but also isolate core identity administration from application infrastructure.

I'd strongly recommend anyone involved with securing, designing or managing Active Directory to familiarize themselves with the concepts and principles of the tiering model, especially the idea of tier 0 equivalency.

Admin accounts all over...

Imagine you've followed all the advice above - provisioned separate admin accounts for all support staff, one for each of the tiers they need to manage, so far so good. Now, all of a sudden, you realize that all this added complexity has presented you with another problem: admin account lifecycle management.

  • Who's gonna remember to check for (and disable!) associated admin accounts when one of your admins leave the company?

Another problem you might encounter with all of these accounts belonging to the same people is identity correlation when triaging suspicious or anomalous activity originating from the administrative accounts.

Say, if a Domain Admin account suddenly deletes a bunch of objects in the directory at 3:45 in the morning and starts changing peoples passwords - you might want to contact the actual employee and ask them to confirm whether this was intended or not - so the ability to automatically translate the identity of an admin account to the associated regular user account might be of importance.
This leaves us with two questions we need to be able to answer:

  • Given the identity of an admin account, return the associated user
  • Given the identity of a user, return any associated admin accounts

I've seen many attempts to do this by comparing usernames, in the hopes that the organization's (often unenforced) naming conventions will save the day ("john-admin probably belongs to john"), and I've seen almost as many failures. We need a more robust way of solving this...

Schema extensions FTW!

In other words, we need some way of persisting a one-to-many relationship between user account objects in Active Directory. Sound familiar?

As it so happens, Active Directory already has a number of attributes that act exactly in this manner. The first example that comes to mind is manager/directReports - these are what we call link-value attributes. When the manager attribute is set on an account to a value of X, the directReports attribute on the object identified by X will automatically reflect the value of the account that was updated. We call the first attribute (manager) the forward-linked attribute of this pair, whereas the second attribute (directReports) is a back-link.

We can leverage this mechanism to create our own such mappings in 3 easy steps:

  • Create a pair of linked attributes in the schema
  • Create an auxiliary class that may contain the attributes
  • Associate the auxiliary class with the existing user object class

attributeSchema

Simply put, every single object in Active Directory consists of an internal, replica-specific identity (a DNT), and a number of attributes that may or may not have values. The behavior of each attribute is in turn bound by an attributeSchema object in the Schema naming-context. If we want to extend the behavior of Active Directory, we need to add some attributeSchema's!

With this in mind, let's start by having a look at the schema definition for the manager attribute:

# Discover the schema container
$rootDSE = Get-ADRootDSE
$schemaNC = $rootDSE.schemaNamingContext

# Discover schema master
$schemaMaster = Get-ADObject $schemaNC -Properties fSMORoleOwner | Get-ADDomainController -Identity { $_.fSMORoleOwner }

# Retrieve the 'manager' attribute
Get-ADObject -Filter 'Name -eq "manager"' -SearchBase $schemaNC -Properties *

adminDisplayName                : Manager
attributeID                     : 0.9.2342.19200300.100.1.10
attributeSyntax                 : 2.5.5.1
DistinguishedName               : CN=Manager,CN=Schema,CN=Configuration,DC=corp,DC=iisreset,DC=me
isMemberOfPartialAttributeSet   : True
isSingleValued                  : True
lDAPDisplayName                 : manager
linkID                          : 42
Name                            : Manager
ObjectClass                     : attributeSchema
oMSyntax                        : 127

In the output above I've listed only a subset of the attributes, but this in essence contains everything we need, specifically:

  • We need to create an attributeSchema object with a name and
  • ... a linkID
  • ... an attributeID
  • ... an oMSyntax value of 127
  • ... an attributeSyntax value of 2.5.5.1

The oMSyntax value 127 indicates that the attribute value is an object reference, and the attributeSyntax 2.5.5.1 means LDAP will use Distinguished Names to represent the object references.

Another point of interest in the output above is the boolean isSingleValued. Given that we're interested in establishing one-to-many relationships, our forward-link to tie admin accounts back to their primary owner should also be single-valued, whereas the back-link must be multi-valued.

Note: member/memberOf is an example of a linked attribute pair in which both links are multi-valued - as they are intended to represent many-to-many relationships

The forward- and back-links are paired by the linkID value - forward-links have even linkID values, and their corresponding back-link attributes will have the same value + 1. In the example above, manager has a linkID value of 42, and sure enough:

PS C:\> Get-ADObject -Filter 'linkID -eq 43' -SearchBase $schemaNC |% ldapDisplayName
directReports

We could just pick some even number, 31706 for example, and assign 31707 to the backlink attribute - and it would probably work - but we run the risk of grabbing an ID that Microsoft might want to use in a future schema update, at which point we'd be in dire trouble.

Since Windows Server 2003, Active Directory supports auto-generating a random linkID pair via a magic OID, by doing the following:

  • Create the forward-link with a linkID value of 1.2.840.113556.1.2.50
  • Reload the schema and retrieve the generated link
  • Create the back-link with a linkID value of the attributeID of the forward link

... and AD will take care of the rest.

The other unique identifiers we need to supply are OIDs for the attributeID values. If your organization already has an ISO OID assigned, you'll want to use an extension of this - but if not, Microsoft provides guidance on how you can generate one.

Finally, a best practice recommendation: add a company-specific prefix to your schema names to avoid collissions with other applications or the base schema! In the following I'll be using the prefix iRM as a shorthand for IISResetMe.

With that out of the way, let's get started!

# Forward-link attribute to indicate the owner of a non-primary account
$fwdAttrName   = "${orgPrefix}-sourceAccount"
$fwdAttrSchema = New-ADObject -Name $fwdAttrName -Type attributeSchema -OtherAttributes @{
    adminDisplayName              = $fwdAttrName
    lDAPDisplayName               = $fwdAttrName
    oMSyntax                      = 127
    attributeSyntax               = "2.5.5.1"
    attributeID                   = "${orgOID}.1"
    isSingleValued                = $true
    isMemberOfPartialAttributeSet = $true
    adminDescription              = "Account owner"
    instanceType                  = 4
    linkID                        = '1.2.840.113556.1.2.50'
    showInAdvancedViewOnly        = $false
} -Server $schemaMaster -PassThru

# Refresh the schema
$rootDSE.Put('schemaUpdateNow', 1)
$rootDSE.SetInfo()

Alright, that's our forward-link, the sourceAccount attribute - this will be used to indicate who's the real owner of an admin account.

Next up, the back-link:

$bckAttrName   = "${orgPrefix}-adminAccounts"
$bckAttrSchema = New-ADObject -Name $bckAttrName -Type attributeSchema -OtherAttributes @{
    adminDisplayName              = $bckAttrName
    lDAPDisplayName               = $bckAttrName
    oMSyntax                      = 127
    attributeSyntax               = "2.5.5.1"
    attributeID                   = "${orgOID}.2"
    isSingleValued                = $false
    isMemberOfPartialAttributeSet = $true
    adminDescription              = "Associated admin accounts"
    instanceType                  = 4
    showInAdvancedViewOnly        = $false
    linkID                        = $fwdAttrSchema.attributeID
} -Server $schemaMaster -PassThru

# Refresh the schema
$rootDSE.Put('schemaUpdateNow', 1)
$rootDSE.SetInfo()

... and that's our back-link, the adminAccounts attribute which can be used to discover the admin accounts associated with an account.

classSchema

Now, the ultimate goal here is to associate the attribute pair with the user object class - but rather than modifying the set of attributes that the user class can contain directly, the recommended method of extending the base schema data types is to create an auxiliary class, and use that to extend the existing type with a collective set of behaviors.

For this reason, our next step is to create such a class:

# Auxiliary class that may contain our attributes
$auxClassName   = "${orgPrefix}-adminAccountExtensions"
$auxClassSchema = New-ADObject -Name $auxClassName -Type classSchema -OtherAttributes @{
    adminDisplayName    = $auxClassName
    lDAPDisplayName     = $auxClassName
    governsID           = "${orgOID}.3"
    mayContain          = @(
        $fwdAttributeSchema.attributeID
        $bckAttributeSchema.attributeID
    )
    objectClassCategory = '3'
    adminDescription    = 'This class adds optional admin account relationship links to user account objects'
    subClassOf          = 'top'
} -Server $schemaNC -PassThru

# Refresh the schema
$rootDSE.Put('schemaUpdateNow', 1)
$rootDSE.SetInfo()

A few points to take note of here:

  • governsID is the class schema identifier, equivalent to attributeID
  • objectClassCategory = 3 means "this is an auxiliary class"
  • We intend to extend the behavior of user, but we're not inheriting from it (hence subClassOf = top)

Finally we need to refresh the schema again one last time for the last operation - tying it all together:

$userClass = Get-ADObject -SearchBase $schemaNC -Filter "Name -like 'user'"
$userClass |Set-ADObject -Add @{
  # add our new auxiliary class to 'user'
  auxiliaryClass = $auxClassSchema.governsID
} -Server $schemaMaster

adminaccountextensions
Our auxiliary class schema as seen in the AD Schema MMC snap-in

Let's see it in action!

In my test environment, the user john has two separate admin accounts:

  • His "Ultra Admin" account, john-ua, for tier 0 administration
  • His "Infra Admin" account, john-ia, for tier 1 administration

To establish the correct relationship using our new attributes, we can use familiar tools like ADAC, dsa.msc or - even better - good old Set-ADUser:

$john = Get-ADUser john
foreach($admin in 'john-ua','john-ia'){
    Set-ADUser $admin -Replace @{ 'iRM-sourceAccount' = $john.distinguishedName }
}

Going forward, you'll want to populate the initial relation during admin account creation, so don't forget to update your provisioning scripts!

Now that the accounts are properly linked, let's revisit the questions I posed earlier:

  • Given the identity of an admin account, return the associated user

Well, now that we literally have that info directly on the object, it becomes as easy as:

PS C:\> $johnIA = Get-ADUser john-ia -Properties iRM-sourceAccount
PS C:\> $johnIA |Get-ADUser -Identity { $_.'iRM-sourceAccount' }

admin-to-user

Nifty! But what about the other way around?

  • Given the identity of a user, return any associated admin accounts

Very well - given the back-link on the source account, that's almost as trivial!

PS C:\> $john = Get-ADUser john-ia -Properties iRM-adminAccounts
PS C:\> $john.'iRM-adminAccounts' |Get-ADUser

user-to-admins

The real value of this - seemingly complex - approach to linking identities come in it's robustness and flexibility though. Any of the accounts involved can have all of their other attributes changed, usernames updated or get moved around the directory without ever breaking this bond we've now created!

I've posted the full script I used to create this schema extension in my lab below if anyone wants to try it on and see if it might help with admin account lifecycle management :)