Create a Custom Schema in 389 Directory Server

Create custom 389 Directory Server attributes and auxiliary object classes with dsconf, schema LDIF files, reload tasks, validation, and replication-safe deployment.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

389 Directory Server custom schema workflow with dsconf attributetypes add, auxiliary object class glcEmployee, and employeeBadgeNumber on a user entry

In this guide, I'll add employeeBadgeNumber through a custom auxiliary object class named glcEmployee, using dsconf schema on instance ldap1 under dc=example,dc=com.

Extend the directory schema only when existing standard attributes and object classes cannot represent the required data. Many deployments already have employeeNumber on inetOrgPerson-style entries. Search the loaded schema before you allocate new OIDs.

Before you start:

IMPORTANT
This guide covers planning, creating, applying, updating, and removing custom attribute types and object classes with dsconf schema and instance schema LDIF files. Entry syntax validation, bulk repair, and OpenLDAP schema import workflows belong in schema validation and repair. Replication overview covers replication agreement configuration. Here you only learn how schema consistency affects replicated data.

Tested on: Rocky Linux 10.2; 389 Directory Server 3.2.0.


Understand attributes and object classes

LDAP schema defines what directory entries may contain. Attribute types describe individual fields; object classes group those fields into entry shapes.

Component Purpose
Attribute type Defines the name, syntax, matching behavior, and value rules
Object class Groups attributes that entries can or must contain
MUST Required attributes on entries using that class
MAY Optional attributes permitted by that class
Structural class Defines the primary type of an entry
Auxiliary class Adds optional capabilities to an existing entry

This guide uses an auxiliary class so you can extend user1 without replacing its existing posixAccount and nsPerson classes.


Plan the custom schema

Every custom attribute and object class needs a globally unique numeric OID. Production deployments must request or use an OID beneath your organization's IANA private enterprise number. Document your allocation range in an internal registry so two administrators do not publish the same number with different meanings.

This lab uses IANA's documentation-only PEN 32473 from RFC 5612. Do not use this branch in a production schema. Replace it with an OID beneath your organization's assigned PEN.

Setting Example
Attribute name employeeBadgeNumber
Attribute OID 1.3.6.1.4.1.32473.1.1.1
Syntax Directory String (1.3.6.1.4.1.1466.115.121.1.15)
Equality matching caseIgnoreMatch (case-insensitive badge lookup)
Single-valued Yes
Object class glcEmployee
Object-class OID 1.3.6.1.4.1.32473.1.2.1
Object-class type Auxiliary
Allowed attribute employeeBadgeNumber (MAY)

Inspect the existing schema

Search for an existing definition before you create a duplicate. A missing attribute query returns no definition line on this version. dsconf may also print a client-side error when the name is unknown:

bash
dsconf ldap1 schema attributetypes query employeeBadgeNumber

Sample output:

output
Error: 'NoneType' object has no attribute 'names'

Query a standard attribute to confirm what is already available. employeeNumber on inetOrgPerson is a common alternative to inventing a badge field:

bash
dsconf ldap1 schema attributetypes query employeeNumber
output
( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'RFC 2798' )

MUST

MAY
( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) X-ORIGIN 'RFC 2798' )

dsconf prints separate MUST and MAY headings. Because employeeNumber is optional on inetOrgPerson, that object class appears under the MAY section. This lab still adds employeeBadgeNumber to demonstrate a controlled custom extension when your application needs a separate field.

Confirm the object class name is unused:

bash
dsconf ldap1 schema objectclasses query glcEmployee
output
None

None means no glcEmployee definition is loaded yet.


Create a custom attribute type

Directory String syntax (1.3.6.1.4.1.1466.115.121.1.15) stores UTF-8 text and is appropriate for badge identifiers such as GLC-1001. Syntax alone does not define how equality searches compare values. You also need an EQUALITY matching rule when applications filter on the attribute or when you build an eq index.

List available syntax OIDs when you need integers, booleans, or distinguished names:

bash
dsconf ldap1 schema attributetypes get_syntaxes

The command prints the full catalog; the Directory String line is Directory String (1.3.6.1.4.1.1466.115.121.1.15).

Add the attribute with caseIgnoreMatch for case-insensitive badge lookups. Use caseExactMatch instead when capitalization must be significant. Do not add a substring matching rule unless applications genuinely need partial badge searches:

bash
dsconf ldap1 schema attributetypes add employeeBadgeNumber --oid 1.3.6.1.4.1.32473.1.1.1 --desc "Employee badge identifier" --syntax 1.3.6.1.4.1.1466.115.121.1.15 --equality caseIgnoreMatch --single-value --x-origin "GoLinuxCloud lab schema"
output
Successfully added the attributeType

Verify the definition:

bash
dsconf ldap1 schema attributetypes query employeeBadgeNumber
output
( 1.3.6.1.4.1.32473.1.1.1 NAME 'employeeBadgeNumber' DESC 'Employee badge identifier' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN ( 'GoLinuxCloud lab schema' 'user defined' ) )

MUST

MAY

SINGLE-VALUE tells the server to reject a second value on the same entry. EQUALITY caseIgnoreMatch enables (employeeBadgeNumber=…) filters.


Create a custom object class

Define the auxiliary class after the attribute exists. Auxiliary classes extend entries without replacing their structural class:

bash
dsconf ldap1 schema objectclasses add glcEmployee --oid 1.3.6.1.4.1.32473.1.2.1 --desc "GoLinuxCloud employee extension" --kind AUXILIARY --sup top --may employeeBadgeNumber --x-origin "GoLinuxCloud lab schema"
output
Successfully added the objectClass

Verify:

bash
dsconf ldap1 schema objectclasses query glcEmployee
output
( 1.3.6.1.4.1.32473.1.2.1 NAME 'glcEmployee' DESC 'GoLinuxCloud employee extension' SUP top AUXILIARY MAY employeeBadgeNumber X-ORIGIN ( 'GoLinuxCloud lab schema' 'user defined' ) )

Schema changes made through dsconf are persisted in /etc/dirsrv/slapd-ldap1/schema/99user.ldif on the tested host. Interactive adds take effect immediately for new LDAP operations on this instance.


Apply the custom schema to an LDAP entry

I'll add the auxiliary class and attribute to an existing user with ldapmodify. The DN must match the stored entry. This lab uses ou=people (lowercase) as created by dsidm:

bash
cat > /tmp/glc-user1.ldif << 'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
add: objectClass
objectClass: glcEmployee
-
add: employeeBadgeNumber
employeeBadgeNumber: GLC-1001
EOF

Apply the change over LDAPI as root:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-user1.ldif
output
modifying entry "uid=user1,ou=people,dc=example,dc=com"

The operation completes without an LDAP error when the schema and DN are correct. Depending on the client options, ldapmodify may print only the entry being modified.

Confirm the entry with ldapsearch:

bash
ldapsearch -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -b "uid=user1,ou=people,dc=example,dc=com" "(objectClass=*)" objectClass employeeBadgeNumber
output
# user1, people, example.com
dn: uid=user1,ou=people,dc=example,dc=com
objectClass: top
objectClass: nsPerson
objectClass: nsAccount
objectClass: nsOrgPerson
objectClass: posixAccount
objectClass: glcEmployee
employeeBadgeNumber: GLC-1001

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

glcEmployee appears alongside the existing person classes, and the badge value is stored on the entry.

Search by badge number to confirm the equality rule works:

bash
ldapsearch -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -b "ou=people,dc=example,dc=com" "(employeeBadgeNumber=GLC-1001)" dn employeeBadgeNumber
output
# user1, people, example.com
dn: uid=user1,ou=people,dc=example,dc=com
employeeBadgeNumber: GLC-1001

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

The filter returns user1. That shows the custom field can be searched, not merely displayed.

EQUALITY caseIgnoreMatch defines how Directory Server compares badge values. It does not create a database index. Create an eq index separately only when the attribute is searched frequently enough to justify one. See index design and maintenance when that chapter is available. An equality matching rule is required to support equality filters and equality indexes, but the index itself is a separate backend configuration.

Test a schema violation

Attributes are permitted only when an entry's object classes allow them. Remove glcEmployee from the entry, then try to add the badge value alone:

bash
cat > /tmp/glc-strip.ldif << 'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
delete: objectClass
objectClass: glcEmployee
-
delete: employeeBadgeNumber
employeeBadgeNumber: GLC-1001
EOF

I'll remove the auxiliary class and stored badge value so I can test schema enforcement on a plain user entry:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-strip.ldif

Re-add only the attribute without the auxiliary class:

bash
cat > /tmp/glc-bad-attr.ldif << 'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
add: employeeBadgeNumber
employeeBadgeNumber: GLC-1001
EOF

This modify should fail because employeeBadgeNumber is not allowed until glcEmployee is present on the entry:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-bad-attr.ldif
output
ldap_modify: Object class violation (65)
	additional info: attribute "employeeBadgeNumber" not allowed
modifying entry "uid=user1,ou=people,dc=example,dc=com"

The server rejects the attribute because no object class on the entry permits employeeBadgeNumber. Restore the working combination before you continue:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-user1.ldif

Add a second badge value to see single-value enforcement:

bash
cat > /tmp/glc-dup.ldif << 'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
add: employeeBadgeNumber
employeeBadgeNumber: GLC-2002
EOF

Directory Server should reject a second value because the attribute is marked SINGLE-VALUE:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-dup.ldif
output
ldap_modify: Object class violation (65)
	additional info: single-valued attribute "employeeBadgeNumber" has multiple values

modifying entry "uid=user1,ou=people,dc=example,dc=com"

For deeper syntax checking across many entries, use schema validation and repair. This guide stops at one illustrative modify failure.


Update or remove custom schema definitions

dsconf schema exposes matching command families for maintenance:

text
dsconf schema attributetypes replace
dsconf schema attributetypes remove
dsconf schema objectclasses replace
dsconf schema objectclasses remove

Follow this order when you retire a custom extension:

  1. Stop using the attribute in directory entries. Remove values and auxiliary classes from affected entries.
  2. Remove the attribute from dependent object-class MUST and MAY lists (or delete the object class).
  3. Update or remove the object-class definition.
  4. Remove the attribute-type definition.

The server blocks removal while schema elements still reference each other. Removing employeeBadgeNumber while glcEmployee still lists it in MAY fails:

bash
dsconf ldap1 schema attributetypes remove employeeBadgeNumber
output
Error: Server is unwilling to perform(53) - attribute type employeebadgenumber: Is included in the MAY list for object class glcEmployee.  Cannot delete.

After you clear entry data, remove the auxiliary class and attribute from user1:

bash
cat > /tmp/glc-cleanup-user.ldif << 'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
delete: employeeBadgeNumber
-
delete: objectClass
objectClass: glcEmployee
EOF

Clear the badge value and auxiliary class from user1 before I remove the schema definitions:

bash
ldapmodify -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd-ldap1.socket -f /tmp/glc-cleanup-user.ldif

Remove the schema definitions in dependency order:

bash
dsconf ldap1 schema objectclasses remove glcEmployee
output
Successfully removed the objectClass

After the object class is gone, I can remove the attribute type it referenced:

bash
dsconf ldap1 schema attributetypes remove employeeBadgeNumber
output
Successfully removed the attributetype

Confirm both definitions are gone:

bash
dsconf ldap1 schema objectclasses query glcEmployee
output
None

The attribute query should also fail once the definition has been removed:

bash
dsconf ldap1 schema attributetypes query employeeBadgeNumber
output
Error: 'NoneType' object has no attribute 'names'

Do not delete schema definitions that entries still use. Clear entry data first, then shrink the schema.


Create a reusable custom schema file

The previous cleanup removed these definitions from 99user.ldif. The following file therefore becomes their only managed schema source on this lab instance.

Schema changes made through dsconf are persisted in 99user.ldif. The server can rewrite schema data in that file; treat it as the live store for interactive changes, not a simple append-only log.

For repeatable deployments across several instances, maintain a separate file under the instance schema directory:

text
/etc/dirsrv/slapd-ldap1/schema/99glc-custom.ldif

Directory Server loads schema files alphabetically. A custom filename must begin with two digits and sort before 99user.ldif. Use a name that also loads after every schema file containing definitions on which your custom schema depends. In this lab, 99glc-custom.ldif loads after the distribution schema files and before 99user.ldif.

Keep custom definitions in your own numeric prefix and leave distribution-managed files untouched. The file begins with the schema entry DN and carries RFC 4512 definitions:

ldif
dn: cn=schema
attributeTypes: ( 1.3.6.1.4.1.32473.1.1.1 NAME 'employeeBadgeNumber' DESC 'Employee badge identifier' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'GoLinuxCloud lab schema' )
objectClasses: ( 1.3.6.1.4.1.32473.1.2.1 NAME 'glcEmployee' DESC 'GoLinuxCloud employee extension' SUP top AUXILIARY MAY employeeBadgeNumber X-ORIGIN 'GoLinuxCloud lab schema' )

Define the attribute before the object class that references it. You can publish the same file through configuration management to every supplier before entries that depend on the schema are written.

WARNING
Keep each definition in one managed source. Do not deliberately maintain the same OID in both 99glc-custom.ldif and 99user.ldif, because duplicate definitions make updates and replication harder to reason about.

Directory Server accepts custom schema through dsconf, the web console, or manually maintained LDIF files. The method should match how your team versions infrastructure.


Reload and verify schema files

After you edit schema LDIF on disk, reload the running server and wait for the task to finish:

bash
dsconf ldap1 schema reload --wait
output
Attempting to add task entry... This will fail if Schema Reload plug-in is not enabled.
Schema reload task (cn=schema_reload_2026-07-15T16:24:29.356062,cn=schema reload task,cn=tasks,cn=config) successfully finished.

--wait blocks until the reload task completes instead of returning while the asynchronous job is still running. If the Schema Reload plug-in is disabled, the task fails. Enable the plug-in before you rely on file-based updates, and check the instance error log when a reload does not finish.

Confirm the definitions are visible to clients:

bash
dsconf ldap1 schema attributetypes query employeeBadgeNumber

I'll query the auxiliary object class as well to confirm the file reload picked up both definitions:

bash
dsconf ldap1 schema objectclasses query glcEmployee

Both commands should print the employeeBadgeNumber and glcEmployee definitions after reload. If ldapmodify reports unknown object class immediately after a file-only change, run dsconf INSTANCE schema reload --wait before retrying the modify.


Maintain schema across replicated servers

Schema added through dsconf is persisted in 99user.ldif with an nsSchemaCSN. Directory Server does not send the change immediately. When the next data update is replicated, the supplier compares schema CSNs and can transfer its compatible schema before sending entries that depend on it.

For reliable schema management, choose one approach:

  • Manage the schema through dsconf and 99user.ldif, allowing compatible schema updates to propagate through replication.
  • Maintain a separate file such as 99glc-custom.ldif, deploy the identical file to every server, and reload it everywhere before writing dependent entries.

Schema definitions kept outside 99user.ldif remain local files and should be deployed consistently to every applicable server; only 99user.ldif participates in normal schema-CSN-based automatic propagation.

Files other than 99user.ldif remain local and are not automatically copied to replication partners. Automatic schema propagation requires every relevant attribute type and object class on the supplier to be a compatible superset of the corresponding definition on the consumer. If the supplier definition is a subset, or the two definitions would need to be merged, Directory Server does not automatically propagate the schema. Avoid making independent, conflicting schema changes on different suppliers.

Confirm schema parity on every supplier before production writes when you add custom attributes that clients depend on. Supplier and consumer configuration lives in replication overview and the dedicated replication chapters. Schema mismatch between peers often appears as replication errors or object class violation on the consumer. Compare dsconf schema output on each supplier when that happens.


Troubleshoot custom schema errors

Symptom Likely cause Fix
Attribute or object class already exists Duplicate dsconf schema … add Query the existing definition; use replace only when you intend to change it
Duplicate OID error on add OID already assigned to another definition Choose a new OID from your documented branch; never reuse OIDs for different meanings
Unknown syntax OID Invalid --syntax value Run dsconf schema attributetypes get_syntaxes and copy a supported OID
Missing superior object class Invalid --sup on object-class add Query a parent class with dsconf schema objectclasses query and reference a loaded name or OID
Attribute not allowed on modify Entry lacks an object class that permits the attribute Add the auxiliary class before the attribute, or extend MAY on the object class
Equality search fails with PROTOCOL_ERROR or UNWILLING_TO_PERFORM The custom attribute has no compatible EQUALITY matching rule Add a matching rule compatible with the syntax, such as caseIgnoreMatch or caseExactMatch, then reload and retry
Missing MUST attribute Structural rules not satisfied Add required attributes, or change the object-class MUST list before creating entries
unknown object class after file edit Running server has stale schema Run dsconf INSTANCE schema reload --wait and recheck error logs
Cannot remove attribute or class Still referenced by entries or other schema Clear entry values, remove MAY/MUST references, then delete definitions
Replication consumer rejects custom entries Schema differs between suppliers Align schema files and reload on all suppliers before replicating new data

Summary

  1. Reuse standard schema whenever an existing attribute such as employeeNumber fits the requirement.
  2. Allocate unique OIDs from your organization's PEN branch, or RFC 5612 32473 for documentation labs only.
  3. Define the attribute type with syntax and equality matching before the object class that references it.
  4. Prefer an auxiliary class when you extend existing person entries.
  5. Test adds, equality searches, and intentional violations on sample entries before production rollout.
  6. Keep schema definitions consistent on every supplier that will hold or replicate the data.

What's Next


References


Frequently Asked Questions

1. When should I create a custom schema instead of using standard LDAP attributes?

Extend the schema only when no standard attribute or object class can represent the data your applications need. Search existing definitions with dsconf schema query before allocating new OIDs.

2. Does adding custom schema require restarting 389 Directory Server?

No for dsconf schema add and replace—the server updates the active schema and persists definitions to the instance schema directory. After editing schema LDIF files manually, run dsconf schema reload --wait so the running server picks up file changes without a full restart.

3. Should custom user attributes use a structural or auxiliary object class?

Prefer an auxiliary object class when you extend existing person entries such as inetOrgPerson or nsPerson. A structural class defines the primary entry type and is harder to layer onto entries that already have a structural class.

4. Where does dsconf store custom schema definitions?

Schema changes made through dsconf are persisted in /etc/dirsrv/slapd-INSTANCE/schema/99user.ldif. You can also maintain separate numbered files such as 99glc-custom.ldif for version-controlled deployment across instances.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …