LDAP Connector Service Account Permissions¶
The LDAP service account used by JIM should follow the principle of least privilege:
- For import only
Grant read access to the containers and attributes that JIM needs to import. - For export (provisioning)
Grant create, modify, and delete permissions on the target containers. For Active Directory, this typically means delegated control over the relevant OUs. - For container provisioning
If Create containers as needed? is enabled, the service account must have permission to create organisational units. - For delta import
The service account needs read access to where the directory keeps its record of changes: theuSNChangedattribute for Active Directory and Samba AD (ordinary read access covers it),cn=accesslogfor OpenLDAP (the access-control rule under OpenLDAP below), and the changelog for 389 Directory Server and generic directories (cn=changelog, or the DN the root DSE'schangelogattribute names; on 389 Directory Server the changelog and plug-in rules under 389 Directory Server below, the second of which is what lets JIM confirm that deletions are being recorded). JIM checks this read at Schema Discovery, where a missing right is a warning, and before every Delta Import, where it is a refusal rather than a run that imports nothing. - For importing deletions (Active Directory)
A Delta Import finds deletions by searchingCN=Deleted Objects,<domain DN>with the Show Deleted Objects control, so the service account needs List Contents, Read Property and Read Permissions over that container. No delegation on an OU reaches it: its permissions are protected from inheritance and its owner is SYSTEM, so even a domain administrator has to take ownership before granting anything. Withholding this read is not refused by the directory: the search succeeds and returns nothing. JIM does not take that silence at face value: it reads the container's own permissions at Schema Discovery and before every Delta Import, and Read Permissions is what lets that check give a definite answer rather than could not tell. See Active Directory below for what JIM does with the answer and for the grant. - For probing generated values
Read access to each attribute JIM probes (for examplesAMAccountName,userPrincipalName,uidormail) across the whole partition, not only the containers JIM imports: a probe searches from the partition root, and an account the service account cannot see is an account the probe cannot find. Where a forest-wide attribute is searched through a Global Catalog, the account needs to read it there too, which authenticated users can by default. JIM detects a probe it cannot trust, because a value it already holds fails to come back, and reports it on the synchronisation's Activity; it then checks that directory against its own records only. See Probing for values already in use. - For setting passwords
Grant the Reset Password permission on the containers JIM manages. In Active Directory this is a control access right, delegated on the OU (Delegate Control, "Reset user passwords and force password change at next logon"), and it is a separate thing from write access to attributes: an account with full write permission on an OU still cannot set a password without it. The service account does not need to be a Domain Admin, and should not be. - For checking reset rights
To answer the reset-rights preflight rather than reporting that it could not tell, the service account also needs read access to thenTSecurityDescriptorattribute of accounts in those containers. Reading an object's permissions is normally covered by ordinary read access; where it is not, the check reports an unknown rather than a denial. - For discovering Fine-Grained Password Policies (Active Directory)
Detecting whether any exist requires read access to the domain's Password Settings Container (CN=Password Settings Container,CN=System,<domain DN>), which by default is restricted to Domain Admins. Without it JIM reports that it could not tell, and treats the domain policy it read as a floor. Granting read on that container is optional; it buys a definite answer in the Password Channel panel and nothing else. - For reading the password policy (389 Directory Server)
Grant read on thepassword*attributes ofcn=config, which hold the global policy; thecn=configrule under 389 Directory Server below grants exactly those. This is optional, and it buys the policy itself: without it the panel reports the configuration as not readable, the override answer is could not tell, and generated passwords follow JIM's defaults rather than the directory's rules, so a stricter rule parks the Connected System Objects it refuses. - For reading the password policy (OpenLDAP)
Two optional reads, each buying something different. Read on the policy entry named byolcPPolicyDefault(and its siblings) gives JIM the rules themselves. Read on the overlay's configuration undercn=config(theolcOverlay=ppolicyentries) tells JIM which policy is the default where more than one exists, and whether a check module is configured; without it, a directory with a single policy entry is still read in full, while one with several is reported as not readable. - For a definite answer on policy overrides (OpenLDAP and 389 Directory Server)
Read access over the objects JIM manages that includes the operationalpwdPolicySubentryattribute (OpenLDAP) or the policy containers (389) lets JIM report that overrides exist. Neither directory offers a way to prove that none do, so the best answer there is could not tell, and the figures shown remain a floor.
Dedicated service account
Always use a dedicated service account for JIM rather than sharing credentials with other applications or using a personal account. This simplifies auditing and ensures that permission changes do not inadvertently affect JIM's operations.
The bullets above say what access JIM needs; the three subsections below are the concrete recipes for granting it. Active Directory delegates on a container, through control access rights and inheritable access control entries. OpenLDAP has none of that machinery: permissions come from olcAccess rules on the directory's own configuration. 389 Directory Server keeps access control instructions as aci values on the entries themselves. The mechanics differ; the goal, least privilege, does not.
Active Directory¶
Bind JIM as a dedicated account, never a Domain Admin. An ordinary user object is enough; JIM needs no rights at all outside the branch it manages.
Grant the access to a group and make the account a member of it, rather than granting anything to the account itself. CN=JIM Connectors,OU=Services,<domain DN> holding CN=svc-jim,OU=Services,<domain DN> is the shape. Replacing the service account, or adding a second one for another Connected System, is then a membership change and the delegation is left alone.
Delegate on the container at the top of the branch JIM manages, normally the OU holding the accounts and groups it provisions. Every entry below is inheritable, so objects and containers created later are covered without a second delegation. That inheritance is also what covers moving an object between containers, which needs delete in the source and create in the target.
These are the access control entries, as SDDL. JIM's own testing runs against exactly these, so they are proven working, not illustrative.
# The access JIM needs over an Active Directory container it manages, as SDDL access control
# entries applied to that container (OU). Everything here is inheritable, so objects created in
# the container later carry it without a second delegation.
#
# __JIM_TRUSTEE_SID__ is the security identifier of the group JIM's service account belongs to.
# Delegating to a group rather than to the account keeps the delegation in place when the
# account is replaced.
#
# Lines starting with # are comments and blank lines are ignored; everything else is
# concatenated in order into one SDDL string.
#
# Class identifiers used below are Active Directory's own, and are the same in every forest:
# bf967aba-0de6-11d0-a285-00aa003049e2 user
# bf967a9c-0de6-11d0-a285-00aa003049e2 group
# bf967aa5-0de6-11d0-a285-00aa003049e2 organizationalUnit
# 00299570-246d-11d0-a768-00aa006e0529 Reset Password (a control access right, not an attribute)
# Create and delete users, groups and organisational units in this container and below. The
# organisationalUnit entry is only needed where "Create Containers as Needed" is switched on;
# leave it out otherwise. Moving an object between containers needs delete here and create there,
# which a single delegation at the top of the managed branch covers.
(OA;CI;CCDC;bf967aba-0de6-11d0-a285-00aa003049e2;;__JIM_TRUSTEE_SID__)
(OA;CI;CCDC;bf967a9c-0de6-11d0-a285-00aa003049e2;;__JIM_TRUSTEE_SID__)
(OA;CI;CCDC;bf967aa5-0de6-11d0-a285-00aa003049e2;;__JIM_TRUSTEE_SID__)
# Read and write the objects themselves: attribute flow, renames, disables and deletes. Read
# Control (RC) is what lets JIM read an object's own permissions, which is how the reset-rights
# preflight answers rather than reporting that it could not tell.
(OA;CIIO;RPWPLCLORCSDDT;;bf967aba-0de6-11d0-a285-00aa003049e2;__JIM_TRUSTEE_SID__)
(OA;CIIO;RPWPLCLORCSDDT;;bf967a9c-0de6-11d0-a285-00aa003049e2;__JIM_TRUSTEE_SID__)
(OA;CIIO;RPWPLCLORCSDDT;;bf967aa5-0de6-11d0-a285-00aa003049e2;__JIM_TRUSTEE_SID__)
# Set passwords on the users in this container. Reset Password is a control access right and is
# separate from write access: without this entry, an account that can write every attribute of a
# user still cannot set that user's password.
(OA;CIIO;CR;00299570-246d-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;__JIM_TRUSTEE_SID__)
On Windows, apply them with dsacls against the container's DN, naming the delegation group as the trustee. Substitute your own domain and container:
dsacls "OU=Corp,DC=corp,DC=local" /I:T /G "CORP\JIM Connectors:CCDC;user"
dsacls "OU=Corp,DC=corp,DC=local" /I:T /G "CORP\JIM Connectors:CCDC;group"
dsacls "OU=Corp,DC=corp,DC=local" /I:T /G "CORP\JIM Connectors:CCDC;organizationalUnit"
dsacls "OU=Corp,DC=corp,DC=local" /I:S /G "CORP\JIM Connectors:RPWPLCLORCSDDT;;user"
dsacls "OU=Corp,DC=corp,DC=local" /I:S /G "CORP\JIM Connectors:RPWPLCLORCSDDT;;group"
dsacls "OU=Corp,DC=corp,DC=local" /I:S /G "CORP\JIM Connectors:RPWPLCLORCSDDT;;organizationalUnit"
dsacls "OU=Corp,DC=corp,DC=local" /I:S /G "CORP\JIM Connectors:CA;Reset Password;user"
Leave the organizationalUnit lines out unless Create containers as needed? is switched on.
The Delegation of Control wizard is the other route to the same place: run it on the container and grant the group "Create, delete and manage user accounts", "Create, delete and manage groups", and "Reset user passwords and force password change at next logon".
Then grant read over the Deleted Objects container, which is where a Delta Import finds deletions and which no delegation on an OU reaches. Its permissions are protected from inheritance and its owner is SYSTEM, so take ownership first:
dsacls "CN=Deleted Objects,DC=corp,DC=local" /takeOwnership
dsacls "CN=Deleted Objects,DC=corp,DC=local" /G "CORP\JIM Connectors:LCRPRC"
List Contents and Read Property are what the tombstone search needs, and JIM only ever reads tombstones. Read Permissions lets the account read the container's own permissions, which is how JIM confirms the grant is in place. Skip this and the directory refuses nothing: the search succeeds and returns nothing. JIM checks rather than trusts that: Schema Discovery, and the schema refresh preview, warn when the account is not allowed to list the container or JIM cannot confirm that it is, and a Delta Import refuses to run when the account is provably not allowed to list it (the Activity says so and names the container), or completes with a warning when JIM cannot confirm. Without Read Permissions the account cannot read the container's own permissions, so JIM can only report that it could not tell; with it, the answer is definite either way.
Read over the Password Settings Container (CN=Password Settings Container,CN=System,<domain DN>) stays optional and is deliberately not in the entries above. Without it JIM reports that it could not tell whether Fine-Grained Password Policies exist, and treats the domain policy as a floor; with it, the Password Channel panel gives a definite answer.
OpenLDAP¶
Bind JIM as a dedicated service account, never the directory's rootDN. Any entry with a userPassword attribute can bind; a typical shape is an organizationalRole plus simpleSecurityObject entry such as cn=svc-jim,ou=Services,dc=example,dc=com.
Access is granted to a group, not to an individual service account's DN. Create cn=jim,ou=Services,dc=example,dc=com (a groupOfNames) on each suffix and add the service account(s) allowed to manage that suffix as a member. Delegating a new Connected System, or adding a second one against the same suffix, is then a membership change, never an ACL edit. A Connected System that imports more than one partition from the same server (one import scoped across two suffix databases) uses a single service account that is a member of each suffix's group, rather than needing a bespoke rule per suffix for that account; OpenLDAP resolves group membership server-wide, so the group and the ACL that names it can live in different databases.
The access-control and password-policy rules below are exactly what JIM's own testing runs under, so they are proven working, not illustrative. Two placeholders run through the access-control files: the suffix DN (__SUFFIX__, for example dc=example,dc=com) and the suffix database's configuration entry (__DB_DN__, its olcDatabase={n}mdb,cn=config DN, found with ldapsearch -b cn=config "(olcSuffix=<suffix>)" dn). The accesslog file's placeholders name the group(s) instead (__JIM_GROUP_DN__, __JIM_GROUP_DN_2__). The ppolicy overlay file's modify form takes the overlay's own DN (__OVERLAY_DN__) rather than __DB_DN__: slapd assigns the overlay an ordinal RDN when it is added (olcOverlay={0}ppolicy,<database DN>, not the unindexed name used to add it), so a later modify has to address the DN slapd actually assigned, found with ldapsearch -b '<database DN>' "(objectClass=olcPPolicyConfig)" dn.
Apply the access-control files bound as the configuration administrator (cn=admin,cn=config), since olcAccess and olcOverlay entries live under cn=config; apply the password policy files bound as the suffix's own administrator, since those are ordinary directory entries. Substitute the placeholders in a copy of each file, then apply it with ldapmodify.
# JIM service account access control: suffix database (issue #1715)
#
# Replaces this database's inherited access-control set with an explicit one
# scoped to exactly what a JIM Connected System needs to do over LDAP:
# import (read), export (add/modify/rename/delete under the managed OUs) and
# password changes (via the RFC 3062 Password Modify extended operation,
# never a direct write to userPassword). Nothing outside these rules is
# granted: every request that falls through them hits "by * none" and is
# refused.
#
# Access is granted to a GROUP, not to an individual service account's DN:
# create cn=jim,ou=Services,__SUFFIX__ (a groupOfNames) and add each JIM
# service account that manages this suffix as a "member" of it. One group
# per suffix. A Connected System that imports more than one partition from
# the same server (for example a single import scoped across two suffix
# databases) uses one service account that is a member of each suffix's
# group, rather than needing a separate ACL per suffix for that account.
# slapd resolves group membership server-wide, so the group and the ACL that
# names it can live in different databases.
#
# Placeholders (substituted by the build script before this file is applied):
# __DB_DN__ - the olcDatabase={n}mdb,cn=config entry for this suffix
# __SUFFIX__ - the suffix DN, e.g. dc=example,dc=com (also where the
# cn=jim,ou=Services,__SUFFIX__ group must exist before this
# file is applied)
#
# Apply this file once per suffix database JIM connects to.
dn: __DB_DN__
changetype: modify
replace: olcAccess
# Rule 0: password changes. JIM never writes userPassword directly; it always
# uses the Password Modify extended operation. Account holders may still
# change their own password over a normal bind, and "anonymous auth" lets a
# simple bind compare the supplied password during authentication.
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group.exact="cn=jim,ou=Services,__SUFFIX__" write by * none
# Rule 1: JIM imports and exports person objects under ou=People.
olcAccess: {1}to dn.subtree="ou=People,__SUFFIX__" by group.exact="cn=jim,ou=Services,__SUFFIX__" write by * none
# Rule 2: JIM imports and exports group objects under ou=Groups.
olcAccess: {2}to dn.subtree="ou=Groups,__SUFFIX__" by group.exact="cn=jim,ou=Services,__SUFFIX__" write by * none
# Rule 3: lets JIM create organisational units directly under the suffix root
# when "create containers as needed" is enabled on the Connected System.
# Optional: if administrators always create target OUs by hand, this rule can
# be left out (or left in place unused) without affecting JIM's other
# operations.
olcAccess: {3}to dn.base="__SUFFIX__" attrs=children by group.exact="cn=jim,ou=Services,__SUFFIX__" write by * none
# Rule 4: read-only access to the rest of the suffix (for example
# ou=Services and ou=Policies below), so JIM's directory browsing and schema
# discovery see a consistent tree without being able to change anything
# outside rules 1-3.
olcAccess: {4}to dn.subtree="__SUFFIX__" by group.exact="cn=jim,ou=Services,__SUFFIX__" read by * none
Rule {3} is what lets JIM create organisational units when Create containers as needed? is enabled; if you always create target OUs by hand, leave the rule out (or leave it unused) without affecting anything else JIM does.
A service account is also subject to the database's search limits, which the rootDN it replaces was not. OpenLDAP applies olcSizeLimit (default 500 entries) across a paged search as a whole, so an import of a container holding more than 500 objects stops at the limit with "The size limit was exceeded", whatever page size the Run Profile uses. Exempt the group from the size and time limits on each suffix; every other client keeps the limits you have set:
# JIM service account search limits: suffix database
#
# OpenLDAP applies the database's size limit (olcSizeLimit, default 500
# entries) to every client that is not the database's rootDN, and it applies
# it across a paged search as a whole, not per page: a JIM import that pages
# through a container of more than 500 objects stops at the limit with
# "The size limit was exceeded" (sizeLimitExceeded, result code 4), however
# small its page size. The rootDN is exempt, which is why an import that
# worked when JIM was bound as cn=admin fails once it is moved to a
# delegated service account.
#
# This file exempts the JIM group from the size and time limits on this
# database, and nothing else: every other client keeps the limits the
# administrator has set. "size=unlimited" lifts the soft, hard and paged
# results (prtotal) size limits together; "time=unlimited" does the same for
# the time limit, since a large import legitimately runs for longer than a
# typical interactive search.
#
# Placeholders (substituted by the build script before this file is applied):
# __DB_DN__ - the olcDatabase={n}mdb,cn=config entry for this suffix
# __SUFFIX__ - the suffix DN, e.g. dc=example,dc=com (the
# cn=jim,ou=Services,__SUFFIX__ group is the one granted
# access in jim-service-account-access.ldif)
#
# Apply this file once per suffix database JIM connects to, bound as the
# configuration administrator.
dn: __DB_DN__
changetype: modify
add: olcLimits
olcLimits: group/groupOfNames/member="cn=jim,ou=Services,__SUFFIX__" size=unlimited time=unlimited
Delta import reads cn=accesslog one level deep. Grant that database's own access-control rule too, naming the group(s):
# JIM service account access control: accesslog database (issue #1715)
#
# JIM's delta import reads cn=accesslog to find what changed since the last
# run (reqStart, reqType, reqDN, reqOld, reqEntryUUID). Only the service
# account(s) that run delta imports may read it; nothing else can, not even
# a suffix's own directory administrator.
#
# A server hosting more than one suffix shares a single accesslog database
# across all of them, so every suffix's cn=jim group needs its own
# "by group.exact=... read" clause here; a single-suffix deployment only has
# one group and should delete the second clause below.
#
# Placeholders (substituted by the build script before this file is applied):
# __DB_DN__ - the olcDatabase={n}mdb,cn=config entry for cn=accesslog
# __JIM_GROUP_DN__ - a suffix's cn=jim,ou=Services,<suffix> group DN
# __JIM_GROUP_DN_2__ - a second suffix's cn=jim group DN (delete this
# clause, and the "by group.exact=... read" it belongs
# to, for a single-suffix deployment)
dn: __DB_DN__
changetype: modify
replace: olcAccess
olcAccess: {0}to * by group.exact="__JIM_GROUP_DN__" read by group.exact="__JIM_GROUP_DN_2__" read by * none
How long the accesslog keeps entries is the overlay's own olcAccessLogPurge (for example 7 days: 07+00:00 01+00:00). Keep that maximum age longer than the longest gap between Delta Imports; a Delta Import that finds the purge has gone past its watermark asks for a Full Import (see Import).
OpenLDAP enforces olcSizeLimit against non-rootDN clients even with paging controls in play, so lift it for JIM on the accesslog database too, or set it unlimited there. A Delta Import the limit stops part-way fails, naming the limit, rather than carry on from a point that could miss a change. A server hosting only one suffix has only one group: delete the second by group.exact=... read clause and its placeholder. Under this rule a suffix administrator can no longer read cn=accesslog themselves; that is intended, since only the service accounts that run delta imports need to. An administrator who needs to inspect it can still bind as the accesslog database's own rootDN.
A frontend rule covers what every client, including anonymous ones, needs for ordinary connection setup:
# JIM service account access control: frontend database (issue #1715)
#
# Rules that are not suffix-specific, so this file has no placeholders and is
# applied once regardless of how many suffixes the server hosts. LDAP clients
# read the root DSE and the subschema subentry as part of ordinary connection
# setup (JIM's discovery reads both while probing server capabilities and
# schema). Neither exposes directory data, so both stay world-readable;
# anything else that would otherwise fall through to the frontend database is
# refused.
dn: olcDatabase={-1}frontend,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to dn.base="" by * read
olcAccess: {1}to dn.subtree="cn=Subschema" by * read
olcAccess: {2}to * by * none
This set withdraws anonymous read and grants nothing to authenticated users other than the service account: OpenLDAP's own default (to * by * read) lets anonymous binds read the whole tree, which these rules deliberately close. If you already have "by users read" style rules for other applications, append them after JIM's rather than replacing JIM's with them, so JIM's by * none fallback stays last.
Password policy¶
JIM discovers the ppolicy overlay's default policy (see Password policy discovery above) and checks a static Initial Password against it before sending. A password the server still refuses parks with the server's own words, exactly as a policy refusal from Active Directory does. The ppolicy overlay only applies to non-rootDN binders, which is another reason to keep JIM off the rootDN: binding it as the service account is what makes password quality checking, lockout, expiry and history apply to JIM's password writes at all.
# JIM password policy: default policy entry (issue #1715)
#
# The default password policy applied to the JIM service account's Password
# Modify operations, so password quality (and any lockout, expiry or history
# settings an administrator later chooses) is enforced the same way it would
# be against a real customer directory. The rootDN is always exempt from
# policy, so this has no effect on a directory administrator's own
# out-of-band changes.
#
# The policy set here is deliberately minimal: a 7-character minimum length
# with basic quality checking; lockout, expiry, history and forced change
# are all switched off. Treat it as a starting point and tighten it to your
# own organisation's password standards.
#
# These are ordinary directory entries (not cn=config), so apply this file
# bound as the suffix's own rootDN, not the configuration rootDN: attaching
# the policy to a database is a separate step (see jim-ppolicy-overlay.ldif)
# that needs the configuration rootDN instead, because olcOverlay entries
# live under cn=config.
#
# Placeholders (substituted by the build script before this file is applied):
# __SUFFIX__ - the suffix DN, e.g. dc=example,dc=com
#
# Requires the ppolicy module to be loaded (olcModuleLoad: ppolicy.so) before
# this file is applied, since the pwdPolicy object class is only registered
# once that module is loaded into a running slapd. Apply once per suffix.
dn: ou=Policies,__SUFFIX__
changetype: add
objectClass: organizationalUnit
ou: Policies
dn: cn=default,ou=Policies,__SUFFIX__
changetype: add
objectClass: top
objectClass: device
objectClass: pwdPolicy
cn: default
pwdAttribute: userPassword
pwdMinLength: 7
pwdCheckQuality: 2
pwdLockout: FALSE
pwdMaxAge: 0
pwdInHistory: 0
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
# JIM password policy: point the ppolicy overlay's default policy (issue #1715)
#
# The lab image (docker/openldap/scripts/01-add-second-suffix.sh) already attaches the ppolicy
# overlay (slapo-ppolicy) to this suffix's database with no default policy, so all this file needs
# to do is point that existing overlay at the policy entry jim-password-policy.ldif creates on the
# same suffix. Applied bound as the configuration rootDN (olcOverlay entries live under cn=config);
# apply after jim-password-policy.ldif has created the policy entry on this suffix.
#
# Placeholders (substituted by the build script before this file is applied):
# __SUFFIX__ - the suffix DN, e.g. dc=example,dc=com
# __OVERLAY_DN__ - the existing olcOverlay=ppolicy entry's own DN. slapd assigns it an ordinal
# RDN when the overlay is added (olcOverlay={0}ppolicy,<database DN>, not the
# unindexed name used to add it), so look it up rather than composing it from
# the database DN: ldapsearch -b '<database DN>' "(objectClass=olcPPolicyConfig)" dn
#
# Requires the ppolicy overlay to already be attached to this database and jim-password-policy.ldif
# already applied to the same suffix. Apply once per suffix database.
dn: __OVERLAY_DN__
changetype: modify
replace: olcPPolicyDefault
olcPPolicyDefault: cn=default,ou=Policies,__SUFFIX__
# --- Only if the ppolicy overlay is NOT already attached to this database ---
# A customer directory that has not already loaded slapo-ppolicy and attached the overlay needs to
# ADD it instead of modifying it; use this in place of the modify above. Unlike the modify, an ADD
# takes the unindexed name (__DB_DN__, the olcDatabase={n}mdb,cn=config entry for this suffix):
# slapd assigns the ordinal RDN itself, so nothing needs looking up first.
#
# dn: olcOverlay=ppolicy,__DB_DN__
# changetype: add
# objectClass: olcOverlayConfig
# objectClass: olcPPolicyConfig
# olcOverlay: ppolicy
# olcPPolicyDefault: cn=default,ou=Policies,__SUFFIX__
389 Directory Server¶
Bind JIM as a dedicated service account, never cn=Directory Manager. Any entry with a userPassword attribute can bind; cn=svc-jim,ou=Services,dc=example,dc=com is the shape. The Directory Manager bypasses every access control instruction and the password policy, which is exactly why it is the wrong identity for JIM.
As on OpenLDAP, access is granted to a group, not to an individual service account's DN. Create cn=jim,ou=Services,<suffix> (a groupOfNames) on each suffix and add the service account(s) allowed to manage that suffix as a member. A Connected System that imports more than one partition from the same server uses a single service account that is a member of each suffix's group; 389 Directory Server resolves group membership server-wide, so the group and the entry carrying the rule can live in different backends.
389 Directory Server denies whatever no rule allows. The three files below are exactly what JIM's own testing applies to 389 Directory Server, so they are proven working, not illustrative. Three placeholders run through them: the suffix DN (__SUFFIX__, for example dc=example,dc=com), the suffix's group DN (__JIM_GROUP_DN__, its cn=jim,ou=Services,<suffix>), and a second suffix's group DN (__JIM_GROUP_DN_2__) in the two files whose rules serve every suffix on the server. Substitute them in a copy of each file, then apply it with ldapmodify bound as cn=Directory Manager.
Two things in the rules will look odd to a reader used to OpenLDAP, and both are deliberate. The rules say targetattr != "aci" rather than targetattr = "*", because on 389 Directory Server * means user attributes only: the operational entryUUID that identifies every object to JIM is only reachable through the negated form. And every rule with an explicit attribute list includes objectClass, because the directory requires the bound account to have search rights on the attribute a filter names, and JIM's base reads use (objectClass=*); without it the read returns nothing at all, with no error.
The suffix rules cover import (read across the suffix), export (add, delete and write under ou=People and ou=Groups) and passwords:
# JIM service account access control: a suffix (389 Directory Server)
#
# Grants a JIM service account exactly what a Connected System does over LDAP
# against one suffix: import (read, search and compare across the suffix, the
# operational attributes such as entryUUID included), export (add, delete and
# write under ou=People and ou=Groups) and password changes (write on
# userPassword, which the RFC 3062 Password Modify operation JIM uses also
# requires of the bound identity). Nothing else is granted: 389 Directory Server
# denies whatever no ACI allows, so a request outside these rules is refused.
#
# Access is granted to a GROUP, not to an individual service account's DN:
# create cn=jim,ou=Services,__SUFFIX__ (a groupOfNames) and add each JIM
# service account that manages this suffix as a "member" of it. One group per
# suffix. A Connected System that imports more than one partition from the same
# server uses one service account that is a member of each suffix's group,
# rather than needing a separate rule set per suffix for that account. 389
# resolves group membership server-wide, so the group and the entry carrying
# the ACI can live in different backends.
#
# Two 389-specific points a reader used to OpenLDAP should know:
# - `targetattr != "aci"` is used rather than `targetattr = "*"` because in
# 389 "*" means user attributes only: the operational attributes JIM reads
# (entryUUID, which identifies every object, and modifyTimestamp) are only
# reachable with the negated form. aci itself stays hidden.
# - Every ACI must let the bound identity search the attribute its filter
# names, and JIM's base reads use (objectClass=*), so any rule with an
# explicit attribute list must include objectClass. The rules here grant
# every attribute but aci, which covers it.
#
# Placeholders (substituted by the build script before this file is applied):
# __SUFFIX__ - the suffix DN, e.g. dc=example,dc=com
# __JIM_GROUP_DN__ - cn=jim,ou=Services,__SUFFIX__ (must exist first)
#
# Apply once per suffix JIM connects to, bound as cn=Directory Manager:
# ldapmodify -x -H ldap://<host>:3389 -D "cn=Directory Manager" -W -f jim-suffix-access.ldif
dn: __SUFFIX__
changetype: modify
add: aci
# Rule 1: import. Read-only access to the whole suffix (ou=Services included),
# so JIM's directory browsing, schema discovery and imports see a consistent
# tree without being able to change anything outside rules 2 to 4.
aci: (targetattr != "aci")(version 3.0; acl "JIM: read the suffix"; allow (read, search, compare) groupdn = "ldap:///__JIM_GROUP_DN__";)
# Rule 2: JIM exports person objects under ou=People (add, modify, rename, delete).
aci: (target = "ldap:///ou=People,__SUFFIX__")(targetattr != "aci")(version 3.0; acl "JIM: manage ou=People"; allow (add, delete, write) groupdn = "ldap:///__JIM_GROUP_DN__";)
# Rule 3: JIM exports group objects under ou=Groups (add, modify, rename, delete).
aci: (target = "ldap:///ou=Groups,__SUFFIX__")(targetattr != "aci")(version 3.0; acl "JIM: manage ou=Groups"; allow (add, delete, write) groupdn = "ldap:///__JIM_GROUP_DN__";)
# Rule 4: password changes. JIM never writes userPassword directly; it uses the
# Password Modify extended operation, which 389 only accepts over a secure
# connection (LDAPS) and which requires write on userPassword of the target
# entry. Rule 2 already grants this; it is spelt out so the grant is explicit.
aci: (target = "ldap:///ou=People,__SUFFIX__")(targetattr = "userPassword")(version 3.0; acl "JIM: set passwords under ou=People"; allow (write) groupdn = "ldap:///__JIM_GROUP_DN__";)
# --- Optional: container creation ("create containers as needed") -----------
# The OpenLDAP recipe lets JIM create organisational units directly under the
# suffix root. 389's ACI language has no one-level scope, and a wildcard target
# (ldap:///ou=*,__SUFFIX__) matches OUs at any depth, so the nearest equivalent
# lets JIM add an organizationalUnit anywhere in the suffix, ou=Services
# included. The lab does not apply it (its managed OUs exist already, and every
# scenario provisions into them). Add it only if administrators will not create
# target OUs by hand:
#
# dn: __SUFFIX__
# changetype: modify
# add: aci
# aci: (target = "ldap:///ou=*,__SUFFIX__")(targetattr != "aci")(targetfilter = "(objectClass=organizationalUnit)")(version 3.0; acl "JIM: create containers"; allow (add) groupdn = "ldap:///__JIM_GROUP_DN__";)
The commented-out rule at the end of the file is the counterpart of the OpenLDAP recipe's rule {3}, for Create containers as needed?. The ACI language has no one-level scope, so it lets JIM add an organisational unit anywhere in the suffix; leave it out if you create target OUs by hand.
Delta Import reads cn=changelog, which the Retro Changelog plug-in creates with no access control at all, so out of the box only the Directory Manager reads it and a Delta Import as anyone else is refused (see Delta Import change source checks). One changelog serves every suffix on the server, so the rule names every suffix's group:
# JIM service account access control: the changelog (389 Directory Server)
#
# JIM's Delta Import reads cn=changelog, the backend the Retro Changelog plug-in
# keeps, to find what changed since the last run (changeNumber, changeType,
# targetDn), and takes its watermark from the rootDSE's lastChangeNumber. The
# plug-in creates cn=changelog with NO access control at all, so out of the box
# only cn=Directory Manager can read it and a Delta Import as anyone else finds
# nothing; JIM checks this read at Schema Discovery (a warning) and before every
# Delta Import (a refusal, rather than a run that imports nothing).
#
# A server hosting more than one suffix shares one changelog across all of them,
# so every suffix's cn=jim group is named here, as further "ldap:///" URLs inside
# the one groupdn value separated by " || ". A single-suffix deployment names one
# group: delete " || ldap:///__JIM_GROUP_DN_2__".
#
# Prerequisites: the Retro Changelog plug-in enabled and the server restarted
# (dsconf <instance> plugin retro-changelog enable), so that cn=changelog exists.
# Recommended alongside it: nsslapd-log-deleted on, so delete records carry the
# deleted entry's attributes (see jim-config-access.ldif for reading it back).
#
# Placeholders (substituted by the build script before this file is applied):
# __JIM_GROUP_DN__ - a suffix's cn=jim,ou=Services,<suffix> group DN
# __JIM_GROUP_DN_2__ - a second suffix's cn=jim group DN
#
# Apply once, bound as cn=Directory Manager:
# ldapmodify -x -H ldap://<host>:3389 -D "cn=Directory Manager" -W -f jim-changelog-access.ldif
dn: cn=changelog
changetype: modify
add: aci
aci: (targetattr != "aci")(version 3.0; acl "JIM: read the changelog"; allow (read, search, compare) groupdn = "ldap:///__JIM_GROUP_DN__ || ldap:///__JIM_GROUP_DN_2__";)
The plug-in has to be on before this rule can be applied, because cn=changelog does not exist until it is. Enable it, turn on the recording of deleted entries (which dsconf does not expose, so it is set on the plug-in entry directly), give the changelog a maximum age longer than the longest gap between Delta Imports, keep userPassword out of it, and restart the directory:
dsconf <instance> plugin retro-changelog enable
dsconf <instance> plugin retro-changelog set --max-age 7d --exclude-attrs userPassword
ldapmodify -x -H ldap://<host>:389 -D "cn=Directory Manager" -W <<'LDIF'
dn: cn=Retro Changelog Plugin,cn=plugins,cn=config
changetype: modify
replace: nsslapd-log-deleted
nsslapd-log-deleted: on
LDIF
Once the plug-in is on, the root DSE advertises changelog, firstchangenumber and lastchangenumber, and JIM uses all three. cn=changelog also appears among the directory's naming contexts, and therefore among the partitions JIM offers on the Connected System; do not select it.
Two reads under cn=config remain, both optional and both read-only: the global password policy, so that the passwords JIM generates satisfy the directory's rules rather than JIM's defaults (Password policy discovery), and the Retro Changelog plug-in's own settings, so that JIM can confirm the plug-in is recording deleted entries rather than warn on every Schema Discovery and Delta Import that it could not. The password policy rule names only the attributes JIM reads. The plug-in rule grants every attribute of that one entry but aci, because 389 Directory Server ships no schema definition for nsslapd-log-deleted and refuses a rule whose attribute list names an attribute the schema does not define; the entry holds nothing but the plug-in's settings.
# JIM service account access control: cn=config (389 Directory Server)
#
# Two narrow, read-only grants under cn=config, both optional: JIM works
# without them, and reports what it could not read.
#
# 1. The global password policy on cn=config itself. JIM reads it so that the
# passwords it generates satisfy the directory's rules rather than JIM's
# defaults, and honours the switches that gate each rule (passwordCheckSyntax,
# passwordHistory, passwordExp). Only the attributes JIM reads are granted,
# plus objectClass: 389 requires the bound identity to have search rights on
# the attribute a filter names, and JIM's read uses (objectClass=*), so
# without objectClass in the list the read returns nothing at all. The
# targetfilter confines the rule to the one entry of class nsslapdConfig,
# so nothing else under cn=config becomes visible through it.
#
# 2. The Retro Changelog plug-in's own settings. JIM reads three of them:
# nsslapd-pluginEnabled (the changelog is on), nsslapd-changelogmaxage (how
# long it is kept; a Delta Import older than that has to be a Full Import)
# and nsslapd-log-deleted, the switch that makes a delete record carry the
# deleted entry's attributes. JIM's Delta Import refuses to run while that
# switch is off, because without it deletions would go unnoticed. 389 ships
# no schema definition for nsslapd-log-deleted, and an ACI whose targetattr
# lists an attribute absent from the schema is refused with "ACL Syntax
# Error", so the rule cannot name it. It therefore grants every attribute of
# that one entry except aci itself: the entry holds nothing but the plug-in's
# settings, so nothing else becomes readable through it.
#
# A server hosting more than one suffix names every suffix's cn=jim group, as
# further "ldap:///" URLs inside the one groupdn value separated by " || ". A
# single-suffix deployment names one group: delete " || ldap:///__JIM_GROUP_DN_2__"
# from both rules.
#
# Placeholders (substituted by the build script before this file is applied):
# __JIM_GROUP_DN__ - a suffix's cn=jim,ou=Services,<suffix> group DN
# __JIM_GROUP_DN_2__ - a second suffix's cn=jim group DN
#
# Apply once, bound as cn=Directory Manager:
# ldapmodify -x -H ldap://<host>:3389 -D "cn=Directory Manager" -W -f jim-config-access.ldif
dn: cn=config
changetype: modify
add: aci
aci: (targetfilter = "(objectClass=nsslapdConfig)")(targetattr = "objectClass || passwordCheckSyntax || passwordMinLength || passwordMinCategories || passwordHistory || passwordInHistory || passwordExp || passwordMaxAge || passwordMinAge || passwordDictCheck || passwordPalindrome || passwordMaxRepeats || passwordMaxSequence || passwordMaxSeqSets || passwordMaxClassChars || passwordMinDigits || passwordMinAlphas || passwordMinUppers || passwordMinLowers || passwordMinSpecials || passwordMin8Bit || passwordMinTokenLength || nsslapd-pwpolicy-local")(version 3.0; acl "JIM: read the global password policy"; allow (read, search, compare) groupdn = "ldap:///__JIM_GROUP_DN__ || ldap:///__JIM_GROUP_DN_2__";)
dn: cn=Retro Changelog Plugin,cn=plugins,cn=config
changetype: modify
add: aci
aci: (targetattr != "aci")(version 3.0; acl "JIM: read the Retro Changelog plug-in settings"; allow (read, search, compare) groupdn = "ldap:///__JIM_GROUP_DN__ || ldap:///__JIM_GROUP_DN_2__";)
Lift the search limits on the account JIM binds as. 389 Directory Server's defaults (nsslapd-sizelimit 2000, nsslapd-lookthroughlimit 5000) apply across a paged search, so a Full Import of more entries than that is subject to them. Set nsSizeLimit, nsLookThroughLimit and nsPagedSizeLimit to -1 on the service account's own entry, as the lab does, rather than raising them server-wide.
Connect over LDAPS. 389 Directory Server accepts the Password Modify operation only over a secure connection (see Setting Passwords), and the suffix rules above grant a password write that it will otherwise refuse with "Confidentiality required".