OUR SITES NetworkRADIUS FreeRADIUS

Sample virtual server for receiving entries from an LDAP directory using the RFC 4533 (LDAP Content Synchronization Operation) in refreshAndPersist mode, Active Directory using its LDAP_SERVER_NOTIFY_OID server control, or a directory implementing Persistent Search as described in https://tools.ietf.org/id/draft-ietf-ldapext-psearch-03.txt

Persistent searches work in a similar way to normal searches except they continue running indefinitely. We continue to receive notifications of changes (add, delete, modify) to entries that would have been returned by a normal search with the base_dn, filter, and scope specified.

The intent of persistent searches is usually to allow directory content to be replicated by another LDAP server, here the primary use case is to receive notifications of changes to entries so we can disseminate that information or act on it.

Note: Each of the three implementations of LDAP synchronisation behave differently:

RFC 4533

This provides a robust mechanism to allow clients to maintain a cached copy of a fragment of a directory by the use of cookies which can be returned to the server indicating the last successfully processed updates from the server.

However, when an object is deleted from the directory, the entry which is received only contains the DN, or, if the deletion is reported as part of the initial refresh phase it may only be the UUID.

Active Directory

Active Directory will only provide updates from the time the query started; there is no mechanism to catch up on changes which occurred while the client was not connected. In addition it is not possible to apply a filter to the query so that only a subset of objects are considered.

If notification is required when objects are deleted, then the Recycle Bin has to be enabled on Active Directory and a query must be running which includes the Deleted Objects container.

Active Directory will only perform persistent searches if the filter is (objectClass=*). To overcome this limitation, FreeRADIUS allows other LDAP filters to be specified which are applied in FreeRADIUS before passing packets to the relevant processing sections.

This implementation of LDAP filters is not intended to be complete, but covers the most likely to be required.

One key limitation, due to not having the LDAP schema to interpret attribute types, is that >=, and bitwise filters are assumed to be on integer values.

If your Active Directory tree contains multiple domains, you will need a query for each domain that is of interest; running a query at the base of the tree with a scope of "sub" does not include any domains other than the base.

Depending on the attributes of interest, the number of notifications of changes received can be reduced by running the LDAP query against the Global Catalog rather than the normal AD LDAP server.

Servers implementing Persistent Search have the option to return the full directory contents, or simply start reporting changes from the point when the query was run.

The draft says that servers SHOULD include a changeNumber when reporting changes to keep track of progress - however this has not been observed in any testing. If this is implemented, then it can behave in a similar manner to the cookie defined in RFC 4533; a search against the change log with a filter of (changeNumber>=n) could be used to read changes since change number 'n'.

Note on user group membership

Many directories provide a virtual memberOf attribute which lists which groups a user is a member of.

With the directories which have been tested, including OpenLDAP and Active Directory, it has been observed that modifying group member lists does not result in notification of changes to the users, even though other modifications to the user will result in a notification which can include the memberOf attribute.

Instead group membership changes are reported as changes to the group object.

Each virtual-server may have multiple listen sections, with each listen section containing multiple sync sections.

The listen section represents a single connection, and a sync represents a single persistent search.

Most options within the listen section are identical to rlm_ldap. See /etc/raddb/mods-available/ldap for more detailed descriptions of configuration items.

Local attributes which are used to cache results from LDAP

The LDAP server to connect to.

May be prefixed with: - ldaps:// (LDAP over SSL) - ldapi:// (LDAP over Unix socket) - ldapc:// (Connectionless LDAP)

Port to connect on, defaults to 389, will be ignored for LDAP URIs.

Administrator account for persistent search. If using SASL + KRB5 these should be commented out.

timeouts may need to be longer than for normal LDAP queries if a refresh phase returns a lot of data.

SASL parameters to use for binding as the sync user.

SASL mechanism

SASL authorisation identity to proxy.

SASL realm. Used for kerberos.

How big the kernel’s receive buffer should be.

Maximum number of updates to have outstanding When this number is reached, no more are read, potentially causing the receive buffer to fill which will cause the change notifications to queue up on the LDAP server

When directories provide cookies to track progress through the list of changes, these can be provided on every update, which can be an excessive rate.

FreeRADIUS keeps track of pending change and will only call store Cookie once the preceding changes have been processed.

These options rate limit how often cookies will be stored. Provided all preceding changes have been processed, cookie Store will be called on a timed interval or after a number of changes have been completed, whichever occurs first.

How often to store cookies.

Number of completed changes which will prompt the storing of a cookie

Persistent searches.

You can configure an unlimited (within reason, and any limitations of the directory you are querying), number of syncs to retrieve entries from the LDAP directory.

Where to start searching in the tree for entries

Only return entries matching this filter Comment this out if all entries should be returned.

Search scope, may be 'base', 'one', 'sub' or 'children'

Specify a map of LDAP attributes to FreeRADIUS dictionary attributes.

The result of this map determines how attributes from the LDAP query are presented in the requests processed by the various "recv" sections below.

This is a very limited form of map: - the left hand side must be an attribute reference. - the right hand side is LDAP attributes which will be included in the query. - only = and += are valid operators with = setting a single instance of the attribute and += setting as many as the LDAP query returns.

Protocol specific attributes must be qualified e.g. &Proto.radius.User-Name

Since there are likely to be multiple members of a given group, the += operator should be used when defining a mapping of LDAP attribute "member" to FreeRADIUS attributes.

If you are querying Active Directory, you are likely to want two queries.

It should be noted that Active Directory will only respond to queries with the LDAP_SERER_NOTIFICATION_OID control if the filter is (objectClass=*) - any other filter will result in an error.

To overcome this limitation, a subset of LDAP filter handling has been implemented in FreeRADIUS allowing a filter to be specified in this configuration. The key limitation is ⇐, >= and bitwise filters assume attributes are numeric.

The only extensible match filters supported are the Active Directory bitwise AND and OR.

A suitable filter, to only receive notifications regarding normal user accounts could be:

(userAccountControl:1.2.840.113556.1.4.803:=512)

In addition, there is nothing returned by Active Directory to distinguish between an object being added or being modified. All LDAP entries which are returned are therefore processed through the recv Modify section when the directory is Active Directory.

By default Active Directory puts a limit of 5 on the number of persistent searches which can be active on a connection.

To determine if an object is enabled or disabled, the attribute userAccountControl can be evaluated. This is returned as string data, so will want mapping to an integer attribute for processing.

Firstly, one based on a naming context which covers all user objects. This will return results when objects are added, modified or restored from the Deleted Objects container.

Secondly, if you have the Recycle Bin enabled in Active Directory and wish to be notified about deleted objects, add a search covering the Deleted Objects container.

This will return results when an object is deleted, however the DN and CN of the object are changed. The attribute lastKnownParent identifies where the object was deleted from. However, lastKnownParent may not be returned when searching the Global Catalog.

Provides FreeRADIUS with the last cookie value we received for the sync

This only applies to directories implementing RFC4533, such as OpenLDAP with the syncprov overlay enabled.

For other directories, this will be called prior to the search query being sent to the server, so could be used for any initial setup of cache datastores.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

You should use these attributes to uniquely identify the sync when retrieving previous cookie values.

In addition the attribute &request.LDAP-Sync.Directory-Root-DN will be populated with the root DN of the directory to aid creating a cookie if one has not previously been stored.

Called for a sync when: - FreeRADIUS first starts. - If a sync experiences an error and needs to be restarted. - If a connection experiences an error and needs to be restarted.

The section may return one of the following/codes attributes: - fail / invalid / reject / disallow to indicate failure. The section will be retried after a delay (the ldap reconnection delay). The sync query will not be started until this section succeeds. - Any other code with &reply.LDAP-Sync.Cookie populated to indicate a cookie value was loaded. - Any other code without &reply.LDAP-Sync.Cookie populated to indicate no cookie was found.

If no cookie is returned for RFC 4533 servers, then the response to the initial search will be a complete copy of the directory. To avoid that, and just be notified about changes, a cookie which matches that which OpenLDAP expects can be mocked up with the following, presuming the ldap module is enabled and configured with the same server settings as ldap_sync.

Stores the latest cookie we’ve received for a sync

This only applies to directories implementing RFC4533, such as OpenLDAP with the syncprov overlay enabled.

For directories implementing persistent search, which return changeNumber in the Entry Change Notice control, this section will be called with the changeNumber in LDAP-Sync.Cookie.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Cookie the cookie value to store.

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

The return code of this section is ignored.

Notification that a new entry has been added to the LDAP directory

For directories implementing RFC 4533, it is recommended that cached entries use LDAP-Sync.Entry-UUID as the key. This can usually be retrieved from the entryUUID operational attribute.

The entryUUID has the benefit that it will remain constant even if an object’s DN is changed.

Delete and Present operations may not include the DN of the object if they occur during a refresh stage.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Entry-UUID the UUID of the object. (RFC 4533 directories only)

  • &request.LDAP-Sync.Entry-DN the DN of the object that was added.

  • &*:* attributes mapped from the LDAP entry to FreeRADIUS attributes using the update section within the sync.

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

  • &request.LDAP-Sync.Attr the attributes returned by the sync (optional).

The return code of this section is ignored (for now).

Notification that an entry has been modified in the LDAP directory

For directories implementing RFC 4533, it is recommended that cached entries use LDAP-Sync.Entry-UUID as the key. This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur during a refresh stage.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Entry-UUID the UUID of the object. (RFC 4533 directories only)

  • &request.LDAP-Sync.Entry-DN the DN of the object that was added.

  • &*:* attributes mapped from the LDAP entry to FreeRADIUS attributes using the update section within the sync.

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

  • &request.LDAP-Sync.Original-DN the original DN of the object, if the object was renamed and the directory returns this attribute.

The return code of this section is ignored (for now).

Notification that an entry has been modified in the LDAP directory

It is recommended that cached entries use LDAP-Sync.Entry-UUID as the key. This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur during a refresh stage.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Entry-UUID the UUID of the object. (RFC 4533 directories only)

  • &request.LDAP-Sync.Entry-DN the DN of the object that was removed (optional).

  • &*:* attributes mapped from the LDAP entry to FreeRADIUS attributes using the update section within the sync, if the attributes are returned by the directory.

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

The return code of this section is ignored (for now).

Notification that an entry is still present and unchanged in the LDAP directory.

These only occur with RFC 4533 servers during initial refresh when a sync starts and a cookie has been provided to indicate the last known state of the directory according to the client.

It is recommended that cached entries use LDAP-Sync.Entry-UUID as the key. This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur during a refresh stage.

A request will be generated with the following attributes:

  • &request.LDAP-Sync.DN the base_dn of the sync.

  • &request.LDAP-Sync.Entry-UUID the UUID of the object. (RFC 4533 directories only)

  • &request.LDAP-Sync.Entry-DN the DN of the object that is still present (optional).

  • &request.LDAP-Sync.Filter the filter of the sync (optional).

  • &request.LDAP-Sync.Scope the scope of the sync (optional).

The return code of this section is ignored (for now).

Default Configuration

server ldap_sync {
	namespace = ldap_sync
	dictionary {
		string	member
		uint64	user-acct-control
		string	last-known-parent
	}
	listen  {
		transport = ldap
		ldap {
			server = "localhost"
#			port = 389
			identity = 'cn=admin,dc=example,dc=com'
			password = mypass
			options {
				res_timeout = 20
				srv_timelimit = 120
				idle = 60
				probes = 3
				interval = 3
				reconnection_delay = 10
			}
			sasl {
#				mech = 'PLAIN'
#				proxy = 'autz_id'
#				realm = 'example.org'
			}
#			recv_buff = 1048576
#			max_outstanding = 65536
		}
		cookie_interval = 10
		cookie_changes = 100
		sync {
			base_dn = "ou=people,dc=example,dc=com"
			filter = "(objectClass=posixAccount)"
			scope = 'sub'
			update {
				&Proto.radius.User-Name = 'uid'
				&Password.With-Header = 'userPassword'
			}
		}
#		sync {
#			base_dn = "ou=groups,dc=example,dc=com"
#			filter = "(objectClass=groupOfNames)"
#			scope = "sub"
#			update {
#				&member += "member"
#			}
#		}
#		sync {
#			base_dn = 'cn=Users,dc=example,dc=com'
#			filter = '(userAccountControl:1.2.840.113556.1.4.803:=512)'
#			scope = 'sub'
#			update {
#				&Proto.radius.User-Name = 'sAMAccountName'
#				&user-acct-control = 'userAccountControl'
#			}
#		}
#		sync {
#			base_dn = "CN=Deleted Objects,dc=example,dc=com"
#			filter = '(userAccountControl:1.2.840.113556.1.4.803:=512)'
#			scope = "one"
#			update {
#				&Proto.radius.User-Name = 'sAMAccountName'
#				&user-acct-control = 'userAccountControl'
#				&last-known-parent = 'lastKnownParent'
#			}
#		}
	}
	load Cookie {
		debug_request
#		if (!&reply.LDAP-Sync.Cookie) {
#			string csn
#			&csn := %concat(%ldap("ldap:///%ldap.uri.safe(%{LDAP-Sync.Directory-Root-DN})?contextCSN?base"), ';')
#			&reply.LDAP-Sync.Cookie := "rid=000,csn=%{csn}"
#		}
	}
	store Cookie {
		debug_request
	}
	recv Add {
		debug_request
	}
	recv Modify {
		debug_request
	}
	recv Delete {
		debug_request
	}
	recv Present {
		debug_request
	}
}